Skip to content

feat(sdk): add TypeScript SDK (@nvidia/openshell-sdk)#2122

Open
maxdubrinsky wants to merge 11 commits into
mainfrom
md/node-sdk-typescript
Open

feat(sdk): add TypeScript SDK (@nvidia/openshell-sdk)#2122
maxdubrinsky wants to merge 11 commits into
mainfrom
md/node-sdk-typescript

Conversation

@maxdubrinsky

@maxdubrinsky maxdubrinsky commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the first native TypeScript SDK for the OpenShell gateway (@nvidia/openshell-sdk), generated from the protobufs over connect-es with no FFI or napi. It covers the v0.1 surface (sandbox lifecycle, health, streamed exec) and wires up codegen, CI, and tagged publishing to GitHub Packages.

Related Issue

Supersedes the shared-FFI-core direction from RFC-0008 (#1764): the frequently-changing surface (RPCs and messages) is already shared through proto/, so each language builds a native client over the generated stubs instead of binding a common Rust core.

Changes

  • sdk/typescript/ — new package. OpenShellClient (create/get/list/delete plus waitReady/waitDeleted, health, streamed exec), transport/auth (h2c + Node TLS + OIDC bearer / CF-Access), and typed errors. Stubs are generated with protoc + @bufbuild/protoc-gen-es (pinned protoc 29.6); src/gen/ and dist/ are gitignored, and dist/ ships the compiled stubs so consumers never regenerate.
  • Tasks / CItasks/typescript.toml (install/proto/typecheck/build/ci/publish); sdk:ts:typecheck added to check; new sdk-typescript job in branch-checks.yml running typecheck, build, and a --dry-run publish that validates the release path.
  • Licensing — SPDX enforcement extended to .ts/.tsx/.mts/.cts (skips node_modules and generated gen/); two back-filled headers.
  • Releaserelease.py gains an npm version format; release-tag.yml publishes to GitHub Packages on tag, stamping the version from the tag (repo keeps a 0.0.0 placeholder). Prerelease builds publish under the next dist-tag, stable under latest.

Distribution note: GitHub Packages forces the @nvidia scope and requires consumers to add a project .npmrc and a token even for public installs. The public API matches the eventual GA package (@openshell/sdk on public npm), so only the install specifier changes at GA.

Open decision (for review)

Package name/scope is unsettled; see the resolvable thread on package.json (the name field). Pre-GA is @nvidia/openshell-sdk; the proposed GA name is @openshell/sdk on public npm.

Testing

  • TS SDK: protoc codegen, tsc typecheck, and build all green; npm publish --dry-run produces a valid 30-file tarball (dist/** + README + package.json) and restores the placeholder.
  • License check green (625 files); ruff clean on changed Python.
  • mise run pre-commit: validated in CI. (Locally the alias also builds/tests the Rust workspace, which the sandbox blocks; that workspace is untouched here.)

Not included: a vitest unit suite and a gateway-gated live e2e test, both follow-ups.

Checklist

  • Follows Conventional Commits
  • Commits are signed off (DCO)
  • Architecture docs — n/a (no published subsystem doc added)

@@ -0,0 +1,46 @@
{
"name": "@nvidia/openshell-sdk",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Naming decision to settle here (resolvable thread).

GH Packages forces the @nvidia scope, so pre-GA ships as @nvidia/openshell-sdk. Proposed GA target: @openshell/sdk on public npm — and reserve the @openshell org now to pre-empt squatting. Public API is identical across the move, so GA is an install-specifier change only.

👍 to confirm, or propose an alternative scope/name.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

@benoitf benoitf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

question: can it generate with namespaces ?

like one sandbox with list() method vs tons or root methods like listSandboxes, createSandbox

const myClient = OpenShellClient.connect(...);
await myClient.sandbox.create(...)
await myClient.sandbox.list(...)
await myClient.gateway.add(...)
await myClient.gateway.list(...)

like the CLI where we have verbs gateway, sandbox, policy, provider , etc.

Comment thread sdk/typescript/package.json Outdated
Comment thread sdk/typescript/package.json Outdated
@maxdubrinsky

Copy link
Copy Markdown
Collaborator Author

question: can it generate with namespaces ?

like one sandbox with list() method vs tons or root methods like listSandboxes, createSandbox

const myClient = OpenShellClient.connect(...);
await myClient.sandbox.create(...)
await myClient.sandbox.list(...)
await myClient.gateway.add(...)
await myClient.gateway.list(...)

like the CLI where we have verbs gateway, sandbox, policy, provider , etc.

@benoitf good question, no we can't but the client isn't generated so namespacing is something we can just do. Refactored this to be closer to the existing Python SDK with a SandboxClient which gets exported from a OpenShellClient class.

maxdubrinsky added a commit that referenced this pull request Jul 7, 2026
Flip the SDK direction from a shared Rust core exposed over FFI
(napi-rs) to native per-language clients generated from proto/. The
wire contract is already shared through proto/ and regenerates cheaply,
so native beats FFI's per-platform-binary and distribution tax on a
thin client.

Keep the openshell-sdk Rust crate, rescoped as the shared transport,
auth, and error core for the Rust consumers only (CLI and TUI). Add a
native language SDK contract (generated stubs, five transport modes,
string-coded errors, curated types with a raw escape hatch) and pin
single-flight OIDC refresh across languages with a conformance suite
instead of a shared binary.

Drop the openshell-sdk-node napi crate; TypeScript (PR #2122) is now
the reference native client and Go is planned. Move the shared-FFI-core
approach into Alternatives with its reasoning preserved, and note that
expanding capability via RPCs is a related track under RFC 0007.

Signed-off-by: Max Dubrinsky <[email protected]>
First native, per-language SDK for the OpenShell gateway: a thin, idiomatic
TypeScript client over proto-generated gRPC stubs (connect-es), no FFI. Covers
the v0.1 surface — sandbox lifecycle (create/get/list/delete + waitReady/
waitDeleted), health, and streamed exec.

- sdk/typescript/: package, client/transport/errors, protoc + protoc-gen-es
  codegen (gen/ gitignored, absorbed into dist/ at build), committed lockfile.
- tasks/typescript.toml: sdk:ts install/proto/typecheck/build/ci/publish;
  sdk:ts:typecheck wired into `check`; sdk-typescript job in branch-checks
  (typecheck, build, and a --dry-run publish that validates the release path).
- Enforce SPDX headers on .ts/.tsx/.mts/.cts (skip node_modules and gen/);
  back-fill docs/_components/jsx.d.ts and fern/components/CustomFooter.tsx.
- release.py gains an npm version format; release-tag.yml publishes to
  GitHub Packages on tag, stamping the version (0.0.0 placeholder in git);
  prerelease builds publish under the `next` dist-tag, not `latest`.

Ships as @nvidia/openshell-sdk on GitHub Packages pre-GA; public npm
(@openshell/sdk) follows at GA with an unchanged public API.

Signed-off-by: Max Dubrinsky <[email protected]>
- typescript ^5.7.2 -> ^6.0.3 (6.0 is now `latest`; the old caret capped at 5.x)
- @types/node ^24.0.0 -> ^24 (same range, tidier)

No source changes; codegen, typecheck, and build pass on 6.0.3. Verified the
emitted d.ts still type-check for downstream consumers on TypeScript 5.0.4
through 5.9.3, so this does not raise the SDK's consumer TS floor.

Signed-off-by: Max Dubrinsky <[email protected]>
Reshape the client from flat methods (createSandbox, listSandboxes, exec) to a
scoped SandboxClient reached as `client.sandbox.create/get/list/delete/exec`
(+ waitReady/waitDeleted), mirroring the CLI's noun-verb model and the Python
SDK's SandboxClient.

SandboxClient is also usable standalone via SandboxClient.connect();
OpenShellClient composes it over a single shared transport, so future
service/provider clients reuse one connection. health() stays top-level as a
gateway call. No behavior change; types are unchanged.

Signed-off-by: Max Dubrinsky <[email protected]>
Replace the protoc gen.sh with `buf generate` + buf.gen.yaml. `buf`
(@bufbuild/buf) is a package devDependency and self-compiles the protos, so
the TS SDK no longer depends on the mise-pinned protoc; it drives the same
connect-es plugin. Generation stays limited to the client-surface closure
(openshell/sandbox/datamodel) via the input paths.

Output is byte-identical to the previous protoc + protoc-gen-es pipeline. Lays
the groundwork for a shared buf.yaml (lint/breaking/LSP) as a follow-up.

Signed-off-by: Max Dubrinsky <[email protected]>
Declare proto/ as a single buf v2 module in a root buf.yaml so buf
generate, lint, breaking, and the editor LSP resolve imports the same
way. Lint uses STANDARD with six documented exceptions for deviations
the current protos intentionally make: the flat proto/ layout with
nested packages (DIRECTORY_SAME_PACKAGE, PACKAGE_DIRECTORY_MATCH) and
the established API shape with unsuffixed services and reused
request/response messages (RPC_REQUEST_RESPONSE_UNIQUE,
RPC_REQUEST_STANDARD_NAME, RPC_RESPONSE_STANDARD_NAME, SERVICE_SUFFIX).
Every other STANDARD rule now enforces on future protos. Breaking uses
FILE.

Code generation stays package-scoped in sdk/typescript/buf.gen.yaml
since it binds to that package's connect-es plugin and output dir;
its inputs are unchanged and regeneration is byte-identical.

Wire the check in via a proto:lint mise task that runs buf from the
SDK devDependencies. It is a dependency of both sdk:ts:ci (so the
TypeScript SDK CI job enforces it) and the top-level lint aggregate
(so local pre-commit covers it).

Signed-off-by: Max Dubrinsky <[email protected]>
Rename the package from @nvidia/openshell-sdk to the unscoped
openshell-sdk and target public npm (registry.npmjs.org) instead of
GitHub Packages. GitHub Packages requires a scope matching the owning
org, and the @openshell scope is blocked by an unrelated existing
package, so an unscoped name on public npm is the lowest-friction
distribution path and needs no org approval.

Rework the release-tag publish job to auth against registry.npmjs.org
with NPM_TOKEN (the job now only needs packages: read to pull the CI
image). Update the README install instructions and usage imports.

Signed-off-by: Max Dubrinsky <[email protected]>
Revert the unscoped-name switch. GitHub Packages only accepts scoped
names matching the owning org, so shipping there first (which needs no
external npm org or NPM_TOKEN, just the repo's GITHUB_TOKEN) requires
the @NVIDIA scope. Keeping the @nvidia/openshell-sdk name also lets a
later public-npm release use the same install specifier, so adding
public npm becomes a second publish step rather than a rename.

Restore the GitHub Packages publish auth in the release-tag job and the
scoped install instructions in the README (keeping the buf codegen
note).

Signed-off-by: Max Dubrinsky <[email protected]>
@maxdubrinsky
maxdubrinsky force-pushed the md/node-sdk-typescript branch from 8272903 to 6908672 Compare July 14, 2026 02:01
@MarsKubeX

Copy link
Copy Markdown

Question: The v0.1 surface covers non-interactive exec() (server-streaming). Are ExecSandboxInteractive (bidi streaming with TTY, stdin, and window resize) and CreateSshSession + ForwardTcp planned for future TS SDK releases? I saw that the Go SDK prototype already wraps these RPCs.

@maxdubrinsky

Copy link
Copy Markdown
Collaborator Author

Question: The v0.1 surface covers non-interactive exec() (server-streaming). Are ExecSandboxInteractive (bidi streaming with TTY, stdin, and window resize) and CreateSshSession + ForwardTcp planned for future TS SDK releases? I saw that the Go SDK prototype already wraps these RPCs.

@MarsKubeX ForwardTcp for sure, this was just a first pass at an SDK with more work to come as needed.

…methods

Grow SandboxClient to the surface the first two consumers need. execStream
yields stdout/stderr chunks as they arrive and exec now drains it, keeping its
buffered ExecResult and signature unchanged. execInteractive is the TTY + stdin
transport primitive (start-first framing, output/write/resize/close/done, no
terminal glue). forward binds a local TCP listener that tunnels each accepted
connection into the sandbox for the process lifetime, minting and revoking a
per-socket SSH session token around a forwardTcp bidi. Adds createSshSession /
revokeSshSession, attach/detach/listProviders, and getConfig / setPolicy /
setSetting (sandbox-scoped, network-policy-only, with an optional wait poll).

Signed-off-by: Max Dubrinsky <[email protected]>
The TypeScript SDK had no formatter or linter and no test runner. Add Biome
(format + lint, generated src/gen excluded) enforcing 2-space indent, single
quotes, semicolons, and a 120-column width, and reformat the existing
hand-written sources accordingly. Add Vitest for unit tests. Wire sdk:ts:format,
sdk:ts:lint, and sdk:ts:test mise tasks into the fmt/lint aggregates, the root
test suite, and sdk:ts:ci so they run in CI.

Signed-off-by: Max Dubrinsky <[email protected]>
Exercise SandboxClient against an in-memory OpenShell service built with
createRouterTransport: request assembly and id resolution, u64/int64 rendered as
strings, enum lowercasing, fromConnect code mapping, the exec/execStream drain
plus a backward-compat check on exec, execInteractive start-first ordering and
done resolution, and a forward() byte relay against a loopback echo with close()
teardown.

Signed-off-by: Max Dubrinsky <[email protected]>
…undaries

Document execStream, execInteractive, forward, ssh sessions, providers, and
config/policy in the SDK README, and record the intentional boundaries:
interactive connect / PTY ownership, upload/download (no file-transfer RPC), and
detached forwards stay out of scope. Note the Biome/Vitest dev commands.

Signed-off-by: Max Dubrinsky <[email protected]>
@drew

drew commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Feedback from my review agent

1. Support the default mTLS gateway

Anchor: sdk/typescript/src/transport.ts:16-27

Could we include client certificate and private-key material in ConnectOptions before publishing this API? caCert only verifies the server; it cannot authenticate the caller. OpenShell's default local Docker, VM, Homebrew, and Linux-package gateway uses mTLS user authentication, so the SDK currently cannot connect to the standard installed gateway at all. createGrpcTransport can pass cert and key through nodeOptions; I would expose those as a validated pair and add an mTLS transport test.

Validation:

  • ConnectOptions exposes only caCert, and buildTransport() passes only ca and rejectUnauthorized to Node TLS.
  • architecture/gateway.md:26-31 defines mTLS user authentication as the default local deployment mode.
  • docs/about/installation.mdx:41-43 and :60-62 document the client mTLS bundle used by installed macOS and Linux gateways.
  • The Rust client passes CA, client certificate, and private key for standard mTLS in crates/openshell-cli/src/tls.rs:399-429.

Confidence: high.

2. Do not make create-time sandbox policy inexpressible

Anchor: sdk/typescript/src/client.ts:46-53

How is a TypeScript caller supposed to create a sandbox with its own filesystem, process, Landlock, or initial network policy? The curated SandboxSpec drops policy, and create() consequently always omits it. setPolicy() cannot repair this later because sandbox-scoped updates require static fields to match the create-time policy. For a sandbox SDK, the safety boundary needs to be expressible at creation; I would expose at least policy, and preferably an advanced full generated spec shape so new template and resource fields do not require an SDK redesign.

Validation:

  • SandboxSpec exposes only name, image, labels, environment, providers, and a boolean GPU request.
  • SandboxClient.create() at sdk/typescript/src/client.ts:363-374 does not populate spec.policy.
  • proto/openshell.proto:313-377 includes policy plus template/runtime/resource controls that the curated type drops.
  • The SDK's own README states at sdk/typescript/README.md:100 that static policy fields must match the create-time policy.
  • The server only permits sandbox-scoped policy differences described in proto/openshell.proto:1203-1208.

Confidence: high.

3. Make streamed command completion observable in normal TypeScript

Anchor: sdk/typescript/src/client.ts:445-483

Could we change the streaming contract so the exit status is observable from idiomatic for await usage? JavaScript discards an async generator's return value when consumed with for await, which is exactly how the README demonstrates execStream(). A failing pytest therefore produces output and then looks successful to the caller. A { output, done } session like the interactive API, or a discriminated stream event that includes the terminal exit, would make failure impossible to miss. The completion path should also reject if the gateway closes without an exit event instead of returning -1.

Validation:

  • execStream() declares its exit-bearing ExecResult as the async generator return value.
  • The README example at sdk/typescript/README.md:58-62 uses for await, so it cannot receive that return value.
  • An isolated Node experiment confirmed that for await sees yielded chunks but discards the generator's terminal return value; only manual next() calls expose it.
  • Both execStream() and execInteractive() initialize the exit code to -1 and accept a clean stream end without a required exit event. The Python SDK rejects this protocol violation at python/openshell/sandbox.py:578-579.

Confidence: high.

4. Make waitReady and waitDeleted honor their timeout

Anchor: sdk/typescript/src/client.ts:412-438

Would these waits ever time out if an individual get() stalls? The deadline is checked only after an unbounded RPC returns, and none of the calls accept an AbortSignal, so a network partition can leave waitReady(name, 30) pending forever. I would pass the remaining deadline into each Connect call and expose cancellation on waits and streams; the timeout argument should bound the returned promise, not just the sleep loop.

Validation:

  • Each loop awaits this.get(name) before checking its wall-clock deadline.
  • get() calls the Connect client without call options, a deadline, or a signal.
  • The same cancellation gap affects exec streams, interactive exec, and forwards, but the wait methods make an explicit timeout promise that is currently not guaranteed.

Confidence: high.

5. Enforce the SSH response trust-boundary contract

Anchor: sdk/typescript/src/client.ts:723-735

Can we validate the CreateSshSession response before returning it? The protobuf contract explicitly requires clients to reject invalid sandbox IDs, tokens, hosts, ports, schemes, and fingerprints because these values feed OpenSSH ProxyCommand construction. This method currently forwards every string from the gateway unchanged, leaving each SDK consumer to rediscover a security invariant that exists only in the proto comments.

Validation:

  • proto/openshell.proto:544-575 says clients must reject responses outside the specified character sets and ranges.
  • createSshSession() performs no response validation.
  • The README presents createSshSession() as the SSH transport primitive, so downstream callers are expected to use these values.

Confidence: high.

6. Respect backpressure on forwarded responses

Anchor: sdk/typescript/src/client.ts:701-705

Could we stop reading the gRPC stream when socket.write() returns false and resume after drain? The current loop keeps pulling sandbox data into Node's socket buffer even when the local client is slow. A large download or long-lived high-throughput forward can therefore grow memory without a bound and eventually take down the SDK process; the tiny echo test does not exercise this path.

Validation:

  • The server-to-local relay ignores the boolean returned by net.Socket.write().
  • Node uses that return value to signal that callers should wait for drain before writing more.
  • The reverse direction has an explicit queue threshold and pause()/resume(), confirming that flow control is part of this implementation's responsibility.

Confidence: high.

Additional API feedback

These do not independently drive the request-changes recommendation, but they are worth settling before the public API becomes expensive to change.

Export a genuinely typed error contract

The PR describes typed errors, but the package exports neither SdkError nor SdkErrorCode; errorCode() returns string | null. Consumers cannot use instanceof, exhaustively switch over supported codes, inspect a preserved Connect status, or distinguish optimistic-concurrency aborts. Export the error class/code union (or a public type guard), preserve the cause/status, and consider a specific conflict/aborted code.

Relevant code: sdk/typescript/src/errors.ts:11-53, sdk/typescript/src/index.ts:35.

Use literal unions for finite SDK states

SandboxRef.phase, Health.status, EffectiveSettingView.scope, and SandboxConfig.policySource are all plain strings even though each comes from a finite protobuf enum. Lowercase literal unions would retain the friendly API while allowing exhaustive TypeScript handling and preventing typos.

Relevant code: sdk/typescript/src/client.ts:41-44, :55-62, :166-181.

Plan for credential lifetime, not only credential injection

oidcToken is captured as one static string. Long-lived agent processes will start failing when it expires, and must replace the whole client. Even if full OIDC refresh remains a follow-up, accepting a per-call async token provider now would avoid freezing a short-lived-token API into the first release.

Relevant code: sdk/typescript/src/transport.ts:16-39.

Do not log any part of an SSH bearer token

The demo prints the first eight characters of the SSH session token. Examples become production code; log only non-secret session metadata.

Relevant code: sdk/typescript/src/demo.ts:86-91.

Align the package-name documentation

The README says the public npm release will use the same package name, while the PR description proposes @openshell/sdk. Resolve the existing naming thread and make the README and release plan agree before publication.

Relevant code: sdk/typescript/README.md:5; existing PR thread on sdk/typescript/package.json:2.

@drew

drew commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Cross-SDK API comparison with Go PR #2271

Compared against PR #2271 at dab9329d983324c90147b0e04f845d02deec01b4.

Conclusion

The approaches are architecturally similar but not yet API-aligned.

Both SDKs use one root client, resource-scoped sub-clients, curated domain types, hidden name-to-ID translation, a shared connection/auth layer, and higher-level operations over generated gRPC clients. That is the same overall SDK pattern.

They diverge at the level consumers will organize applications around. The Go API treats exec, files, SSH, TCP, configuration, policy, services, providers, health, and sandboxes as peer domains. The TypeScript API puts lifecycle, exec, TCP listening, SSH sessions, provider attachments, sandbox configuration, and policy updates on one SandboxClient, leaving only health at the root. This is more than naming or language idiom; it is a different capability taxonomy.

PR #2271 also declares the intended full Go surface upfront while implementing only sandbox operations in this PR. Exec, files, health, providers, policy, services, TCP, SSH, and configuration currently return Unimplemented and are scheduled for follow-up PRs. The comparison below therefore uses the Go interfaces as the intended API contract, not as a claim that every operation works at this commit.

Capability mapping

Area TypeScript PR #2122 Go PR #2271 intended API Alignment
Root composition Root client plus one sandbox sub-client; health stays top-level Root client with peer sub-clients for every resource domain Same pattern, different taxonomy
Sandbox lifecycle Create, get, list, delete, wait-ready, wait-deleted Create, get, list, delete, wait-ready, watch, logs Core CRUD matches; observability and deletion waiting differ
Sandbox result Lean SandboxRef: ID, name, phase, labels, resource version Full Sandbox: metadata, complete spec, status, conditions, current policy version Materially different
Create input Flattened image/environment/providers/GPU boolean; no policy or advanced template fields Full policy, template, runtime class, resources, user namespaces, driver config, GPU count Materially different
Name versus ID Public methods consistently accept sandbox names and resolve IDs internally Mostly name-based with internal resolution; raw SSH session creation is explicitly ID-based Largely aligned
Buffered exec Returns exit code and byte buffers Returns exit code and byte slices Aligned
Streaming exec Async iterable whose hidden generator return carries the exit code Stream object with explicit Next, ExitCode, and Close lifecycle Same capability, different completion semantics
Interactive exec Output iterable plus write, resize, close, and completion promise Read/write session plus resize, close, and explicit exit status Closely aligned
TCP forwarding sandbox.forward() creates a process-local listener and returns its bound address Separates one tunneled connection (TCP.Forward) from a local listener (TCP.Listen) Materially different abstraction
SSH Create/revoke session by sandbox name under the sandbox client Separate SSH domain: raw ID-based session plus name-based managed tunnel Partial overlap
Provider attachment Attach, detach, list on the sandbox client Same operations on the sandbox client Aligned
Provider management Not exposed Separate provider CRUD/ensure, profiles, and refresh domains Missing in TypeScript
Configuration Sandbox get/set-policy/set-setting methods Separate sandbox/global configuration domain with a general update model Partial overlap; different ownership
Policy Only full sandbox policy update through the sandbox client Initial policy plus separate draft, approval, status, and revision-history domain Initial-policy gap plus much broader Go surface
Services Not exposed Separate HTTP service endpoint domain Missing in TypeScript
Files Explicitly out of scope First-class upload/download domain planned Product-boundary disagreement
Health Top-level health() Health sub-client with Check Semantically aligned
Watch and logs Polling waits only Typed sandbox watch and filtered log retrieval Missing in TypeScript
Cancellation/deadlines No public cancellation or per-call deadline Every operation receives cancellation/deadline context Material behavioral difference
Client lifecycle No close/dispose contract Idempotent client close Material lifecycle difference
TLS CA trust and insecure verification; no client certificate/key CA plus validated client certificate/key pair Go covers default OpenShell mTLS; TypeScript does not
Auth lifetime Static OIDC or edge token Pluggable per-call auth, static token, refreshable token, extra headers Same layer, substantially different capability
Errors Internal SdkError; public string parser Public typed status error, stable code set, predicate helpers, details Different public contract
Wire isolation Most types curated, but policy and setting messages are generated protobuf types Domain package has no protobuf imports Similar intent, different boundary strength

Shared high-level concepts worth preserving

These concepts are consistent enough to become a cross-language SDK contract:

  • A single root client owns one shared gateway connection and composes resource clients.
  • Sandbox-facing operations accept the canonical sandbox name and hide RPCs that require the sandbox ID.
  • Lifecycle has create/get/list/delete plus a readiness primitive.
  • Exec has buffered, incremental, and interactive forms, all reporting a required terminal exit status.
  • Provider attachment is a sandbox operation and supports optimistic concurrency.
  • Port forwarding and SSH session credentials are managed resources with explicit cleanup.
  • Configuration and policy expose revision metadata and preserve 64-bit values without lossy numeric conversion.
  • Generated transport types should not become the default public application model.

Decisions needed for alignment

  1. Choose one resource taxonomy. The Go domain split scales better as the API grows. TypeScript can still use idiomatic property names, but exec, TCP, SSH, configuration, policy, providers, services, files, health, and sandboxes should have the same conceptual ownership across SDKs.

  2. Standardize the sandbox domain object. Decide whether SDK CRUD returns a lightweight reference or the full desired/observed sandbox. Returning different information in each language will force cross-language examples and orchestration code to behave differently.

  3. Standardize the create contract. Every SDK needs to express the full safety-relevant create-time policy and an escape hatch for current template/resource fields. The Go shape demonstrates the semantic surface missing from TypeScript.

  4. Define streaming completion independently of iteration syntax. Every streaming exec must expose an unavoidable terminal exit status, cancellation, and cleanup. The exact iterator mechanics can remain language-specific.

  5. Define two TCP primitives explicitly. A single tunneled byte stream and a bound local multi-connection listener are different resources. The current TypeScript forward corresponds to Go Listen, not Go Forward.

  6. Agree on operational parity. Default mTLS, refreshable auth, cancellation/deadlines, typed errors, and client shutdown are baseline SDK behavior rather than language-specific conveniences.

  7. Settle the file-transfer boundary. Go declares upload/download as first-class SDK operations while TypeScript explicitly excludes them because the gateway has no file RPC. Either choice can work, but cross-language SDKs should not disagree about whether tar-over-SSH belongs in the SDK contract.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants