Skip to content

feat(sdk): add openshell-sdk crate#1862

Merged
drew merged 8 commits into
mainfrom
md/openshell-sdk-crate
Jul 15, 2026
Merged

feat(sdk): add openshell-sdk crate#1862
drew merged 8 commits into
mainfrom
md/openshell-sdk-crate

Conversation

@maxdubrinsky

@maxdubrinsky maxdubrinsky commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

First focused PR split out of #1617. Adds a new openshell-sdk crate: the shared async Rust client for OpenShell gateways. It owns gRPC channel construction, TLS material handling, request interceptors, OIDC single-flight token refresh, and the Cloudflare Access edge tunnel, and exposes a sandbox-focused high-level surface plus a raw escape hatch.

The crate is additive (nothing consumes it yet, so no behavior change). The one non-additive piece is a behavior-preserving extraction: the signature-unverified JWT exp decoder moves out of openshell-core/src/grpc_client.rs into a shared openshell_core::jwt module, so the sandbox-side client and the SDK's refresh path derive token expiry from one place instead of each reimplementing it. Moving openshell-cli and openshell-tui onto the crate is a separate follow-up PR.

Related Issue

Changes

  • New crates/openshell-sdk/:
    • OpenShellClient: high-level sandbox surface (health, sandbox CRUD, readiness and deletion waits, non-streaming exec).
    • Transport stack: channel construction and the five transport/auth modes the CLI exercises today (plaintext, mTLS, OIDC bearer over HTTPS, Cloudflare Access tunnel, insecure TLS).
    • OIDC: single-flight refresh wired into the client auth path (proactive before unary calls, reactive on Unauthenticated with one retry), behind a napi-free Refresh trait so the core stays binding-agnostic.
    • raw module: re-exports the generated tonic clients for RPCs the curated surface does not yet cover.
    • SdkError taxonomy and curated types (SandboxSpec, SandboxRef, Health, ListOptions, ExecOptions).
    • Crate README.md.
  • Shared JWT-exp decoder: new openshell_core::jwt::parse_exp_secs, extracted from grpc_client.rs (which now calls it). The SDK reuses it to derive a proactive-refresh deadline from a bearer JWT when the caller does not advertise expires_at, so consumers do not reimplement JWT expiry parsing per language.
  • AGENTS.md: architecture-table row for the new crate.
  • Cargo.lock: additive (the crate registers via the existing members = ["crates/*"]).

No changes to openshell-cli or openshell-tui. The only openshell-core change is the JWT-exp extraction above; the transport code the CLI/TUI own today (openshell-core/src/auth.rs, openshell-cli/src/edge_tunnel.rs, tls.rs, oidc_auth.rs) is untouched here and migrates onto the SDK in the follow-up PR, where the now-duplicated code is removed.

Testing

  • cargo build --workspace, cargo clippy --workspace --all-targets -- -D warnings, and cargo fmt --all -- --check are clean.
  • cargo test -p openshell-sdk: 9 unit + 12 mock-gateway tests pass (incl. single-flight cancellation-safety, Debug-redaction, and token-encoding regression tests).
  • cargo test -p openshell-core: 299 pass, including 3 new jwt decoder tests. The pre-existing grpc_client expiry tests still pass, confirming the decoder extraction is behavior-preserving.
  • license:check and markdown:lint pass.
  • E2E not applicable. Additive crate with no consumers and no behavior change; the mock-gateway tests are the regression surface for the client.

Checklist

  • Follows Conventional Commits
  • Commits are signed off (DCO)
  • Architecture docs updated (if applicable): AGENTS.md architecture table and the crate README.md.

@maxdubrinsky maxdubrinsky requested review from a team, derekwaynecarr and mrunalp as code owners June 10, 2026 21:28
@maxdubrinsky maxdubrinsky force-pushed the md/openshell-sdk-crate branch from aef5832 to d1668e2 Compare June 12, 2026 18:05
@maxdubrinsky maxdubrinsky force-pushed the md/openshell-sdk-crate branch from d1668e2 to d8362c4 Compare June 26, 2026 15:44
@copy-pr-bot

copy-pr-bot Bot commented Jun 26, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@maxdubrinsky maxdubrinsky force-pushed the md/openshell-sdk-crate branch from d8362c4 to 5d5cfe9 Compare June 26, 2026 16:42
@drew drew added the test:e2e Requires end-to-end coverage label Jun 26, 2026
@github-actions

Copy link
Copy Markdown

Label test:e2e applied, but pull-request/1862 is at d1668e2 while the PR head is 5d5cfe9. A maintainer needs to comment /ok to test 5d5cfe9a0dd5aa2dfd006cc5c43788245a3a6065 to refresh the mirror. Once the mirror catches up, re-run Branch E2E Checks from the Actions tab.

@drew

drew commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

/ok to test 5d5cfe9

Comment thread crates/openshell-sdk/src/refresh.rs
@maxdubrinsky maxdubrinsky force-pushed the md/openshell-sdk-crate branch from a60554a to d932183 Compare June 30, 2026 15:52
@maxdubrinsky maxdubrinsky changed the title feat(sdk): extract openshell-sdk crate feat(sdk): add openshell-sdk crate Jun 30, 2026
Additive extraction of the shared async gRPC client core (transport, TLS,
OIDC single-flight refresh, edge tunnel, high-level sandbox surface, raw
escape hatch) as a new workspace crate. No existing consumers yet; CLI/TUI
migration follows in a separate PR.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
Extract the signature-unverified JWT exp decode out of
openshell-core/grpc_client.rs into openshell_core::jwt::parse_exp_secs,
and have the openshell-sdk refresh path reuse it to derive a proactive
refresh deadline from a bearer JWT when the caller does not advertise
expires_at. Addresses review feedback to reuse pre-existing logic rather
than reimplement JWT expiry handling per client.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
Addresses review feedback on the openshell-sdk refresh path.

- Make the single-flight cleanup cancellation-safe. The in-flight slot is
  now cleared by the shared refresh computation itself (epoch-guarded)
  rather than the leader's post-await code. Previously, if the leader future
  was dropped (e.g. an FFI caller cancelling its promise) after a follower
  drove the refresh to completion, the completed future was stranded in the
  slot and later refresh_now() calls re-joined it, pinning the client to a
  stale or already-rejected token. Adds a regression test that cancels the
  leader and asserts the next refresh starts a fresh attempt.

- Redact bearer secrets from Debug. RefreshedToken and the oidc
  RefreshTokenInput/RefreshTokenOutput now use manual Debug impls that omit
  the access/refresh token fields via finish_non_exhaustive, matching the
  house style (e.g. SecretResolver, SandboxJwtIssuer). Prevents a stray
  {:?} or a containing struct's derived Debug from writing tokens to logs.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
store_bearer now returns an error instead of silently keeping the previous
bearer value. The TokenSource commits the refreshed token to its state
before the client writes it into the interceptor slot, so a silent drop
left the interceptor on the old (expiring) token with no path back to a
refresh. Surfacing the error fails the call loudly instead. Adds a unit
test covering a token that can't be encoded as gRPC metadata.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
Raw gRPC access never triggered OIDC refresh: a client that only used
raw_grpc/raw_inference kept sending the initial bearer until it expired,
with no proactive or reactive refresh. Add raw_grpc_fresh and
raw_inference_fresh accessors that refresh before returning the client,
plus force_refresh for reactive recovery after an Unauthenticated raw
RPC.

Guard the single-flight refresh commit against a concurrent replace().
The in-flight attempt now records the generation it started from and
skips its write when an external replace() has advanced it, so timer or
callback driven rotation is no longer clobbered by a slower refresh.

Remove TokenSource::snapshot(): it returned an empty string under write
contention and had no consumer on the CLI/TUI path. Tests read committed
state directly instead.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
Recast the openshell-sdk README as a usable crate README rather than an
RFC excerpt. Drop the Responsibilities/Non-responsibilities/Consumers
scope-boundary sections and the mTLS migration rationale, folding the
useful facts (explicit token, no disk/name resolution, Refresh trait,
SdkError mapping) into the intro, a new Auth and refresh section, and
Public surface. Remove the dead relative RFC link and status-label
prose so the doc renders cleanly wherever it is published.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
@maxdubrinsky maxdubrinsky force-pushed the md/openshell-sdk-crate branch from 7bfe820 to ce7ef91 Compare July 9, 2026 16:23

@drew drew left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agent review

crates/openshell-sdk/src/client.rs: [nit] Error-variant mappings undermine the crate's own SdkError::code() discriminant contract: wait_ready/wait_deleted timeouts (lines 245, 384) and a sandbox entering the Error phase (line 224) are reported as SdkError::Connect when no connection failed, and sandbox_from_response (line 471) maps a malformed server response to InvalidConfig although the caller's configuration is fine. A binding consumer switching on code() (advertised in error.rs as the stable cross-language discriminant) would misinterpret a provisioning timeout as a connectivity failure. Dedicated variants (e.g. Timeout) or at least Rpc would be more honest.

Comment thread crates/openshell-sdk/Cargo.toml Outdated
Comment thread crates/openshell-sdk/src/client.rs Outdated
Comment thread crates/openshell-sdk/src/client.rs
Comment thread crates/openshell-sdk/src/config.rs Outdated
Comment thread crates/openshell-sdk/src/oidc.rs Outdated
Comment thread crates/openshell-sdk/src/refresh.rs Outdated
Comment thread crates/openshell-sdk/src/transport.rs Outdated
Comment thread crates/openshell-sdk/src/transport.rs
Apply Drew's review notes on PR #1862:

- Drop unused `rustls-pemfile` dependency and move `tokio-stream` to
  dev-dependencies (only used by tests).
- Guard OIDC `expires_at` against u64 overflow with `saturating_add`.
- Fix stale `#[non_exhaustive]` rationale in `AuthConfig` (the struct
  `Oidc` variant it described as future already ships).
- Stop double-wrapping refresh errors: store the bare refresh-error text
  so the single `SdkError::auth` wrap happens once at await.
- Strip stale CLI porting breadcrumbs from `build_channel` docs, keeping
  the branch table.
- Treat proactive token refresh as best-effort: a transient failure falls
  through to the request instead of failing an RPC whose current token is
  still valid, with a regression test.
- Collapse `exec`'s inline auth retry into the shared `unary` helper.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
@drew

drew commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Review follow-up at de733448:

  • Resolved seven inline threads addressed in cc9e53c3 / de733448.
  • Left the refresh-error thread open: the double wrapping is fixed, but Transient and Terminal still collapse into the same SdkError::Auth discriminant.
  • Left the edge-tunnel double-start race open: it is still present and remains deferred to follow-up migration/consolidation work.
  • The review-body SdkError mapping concern remains unaddressed: sandbox wait timeouts/error phases still map to Connect, and a missing sandbox in a gateway response still maps to InvalidConfig.

The refresh single-flight collapsed both `RefreshError::Transient` and
`RefreshError::Terminal` into a stringified `SdkError::Auth`, so consumers
(CLI, TUI, future language bindings) had no machine-readable way to tell a
retryable IdP blip from a dead session that needs re-authentication.

Carry the `RefreshError` through the shared outcome (kept `Clone` for
`Shared`) instead of its rendered text, and map it at the await site to a
new `retryable` flag on `SdkError::Auth`. Add `SdkError::auth_retryable`
and a `SdkError::retryable()` accessor; transient refresh failures report
`true`, every other error `false`. Add classification tests.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
@drew drew merged commit e6f319c into main Jul 15, 2026
46 checks passed
@drew drew deleted the md/openshell-sdk-crate branch July 15, 2026 05:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test:e2e Requires end-to-end coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants