Repository navigation
Description
Problem Statement
The BFF gateway layer duplicates functionality that the official OpenShell Go SDK now provides. This creates unnecessary maintenance overhead: 57 hand-written RPC wrappers, local proto stubs, manual error wrapping, and a custom auth mechanism — all of which the SDK handles upstream. This issue proposes an incremental migration to eliminate that duplication.
Why
The BFF maintains a hand-written gRPC client wrapper (backend/internal/gateway/client.go + 9 domain files) that directly imports proto-generated stubs and manually wraps 57 RPC methods with fmt.Errorf context. This is a maintenance burden:
-
Dual maintenance. Every proto change upstream requires regenerating stubs in this repo and updating the wrapper layer. The official Go SDK (
github.com/NVIDIA/OpenShell/sdk/go, merged in NVIDIA/OpenShell#2271) already provides typed sub-clients, domain types, and proto converters — maintained upstream. -
No test fakes. The current interface is mocked by a manually-written
mockGatewaywith injectable function fields. The SDK ships afakepackage with in-memory stores, watch broadcasting, and deep-copy isolation — free testing infrastructure. -
Fragile error extraction. The BFF wraps every gRPC error with
fmt.Errorf("verb resource: %w", err), then must unwrap it inwriteGrpcErrorviaerrors.As+GRPCStatus(). The SDK surfaces errors asStatusErrorwith typed helpers (IsNotFound(),IsAlreadyExists()), making the unwrap/re-wrap cycle unnecessary. -
Auth duplication. The BFF implements its own
PerRPCCredentialsfor token forwarding. The SDK providesAuthProviderimplementations (including refreshable tokens) that handle this at the transport layer.
What This Means Today
The SDK foundation (#2271) is merged. The completion PR (NVIDIA/OpenShell#2702) adds all 14 domain clients including Workspace, Inference, Provider, Exec, Policy, and Service — every RPC the BFF currently wraps. It is open, mergeable, and under review.
Settled Design Decisions
1. Adapter pattern behind gateway.Interface
The existing gateway.Interface (57 methods) remains the contract between API handlers and the gateway layer. The migration replaces the implementation behind it, not the interface itself. This means:
- API handlers do not change.
- The mock in
mock_gateway_test.gocontinues to work. - Migration can be incremental (one sub-client at a time).
2. Per-request auth via custom AuthProvider
The BFF forwards a different bearer token per HTTP request (from the auth proxy). The SDK's AuthProvider is set at client construction time. Resolution: implement a context-aware AuthProvider that reads the token from the request context on each RPC:
type contextAuth struct{}
func (a *contextAuth) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
token := auth.TokenFromContext(ctx)
if token == "" {
return nil, nil
}
return map[string]string{"authorization": "Bearer " + token}, nil
}
func (a *contextAuth) RequireTransportSecurity() bool { return false }This preserves the BFF's relay-only architecture (ADR 0014) while using the SDK's transport layer.
3. No proto stubs in this repo after migration
Once fully migrated, backend/gen/ (generated proto stubs) can be deleted. The SDK owns the proto-to-domain conversion internally. The BFF consumes only SDK domain types.
Proposed Design
Migration phases
Phase 1: Sandbox client (immediate)
- Replace
gateway/sandboxes.go(5 methods) with SDKSandboxes()calls. - Verify: auth forwarding works, error types map to existing HTTP codes, streaming
ExecSandboxresponse shape unchanged. - After merge: BFF uses SDK for sandbox CRUD; raw gRPC for everything else.
Phase 2: Providers + Profiles + Refresh (after SDK #2702 merges)
- Replace
gateway/providers.go(15 methods). - Largest surface area; validates that the converter types align.
Phase 3: Workspaces + Policy + Services + Inference
- Replace remaining domain files.
- After merge:
backend/gen/is dead code.
Phase 4: Cleanup
- Delete
backend/gen/, remove proto generation tooling from the repo. - Remove
go.sumentries for direct proto dependencies. - Update
go.modto depend only ongithub.com/NVIDIA/OpenShell/sdk/go.
Cross-cutting constraints
- No API contract change. HTTP response shapes must be identical before and after. Validated by existing
respond_ext_test.goand e2e tests. - Error code preservation. The SDK's
StatusErrorcodes must map to the same HTTP status codes the BFF currently returns. Verified viaTestWriteGrpcErrorWrapped. - Streaming passthrough.
ExecSandboxInteractivemust remain a bidirectional stream bridge (WebSocket to gRPC). The SDK'sExec().Interactive()returns anInteractiveSessionwithRead/Write/Resize/Close— verified compatible (see below).
Compatibility Verification
The following were verified against NVIDIA/OpenShell main and openshell-dashboard at 8bf7a76:
| Check | Result | Detail |
|---|---|---|
| Workspace parameter pattern | Compatible | SDK uses (ctx, workspace, name string, ...) — same as BFF |
| ExecResult type | Compatible | SDK: {ExitCode int, Stdout []byte, Stderr []byte} — identical to BFF aggregator |
| InteractiveSession vs raw stream | Compatible | SDK Read/Write/Resize maps to BFF Send(Stdin)/Send(Resize)/Recv(Stdout+Stderr). Stdout and stderr are mixed in Read() which is correct for terminal (xterm.js renders both to same output) |
| grpc-go version | Compatible | SDK v1.80, BFF v1.82. Go modules resolves to v1.82 (semver minor, backward compatible) |
| Inference client | Blocked on #2702 | inference.proto exists in OpenShell but SDK client only comes with PR #2702. Keep raw gRPC for 3 Inference methods until then |
Alternatives Considered
Keep the raw gRPC client. Zero effort now, ongoing tax every time the proto evolves. Every new RPC requires: regenerate stubs, write wrapper method, add to interface, update mock. With the SDK: go get -u and call the new method.
Wait for SDK to be declared stable. The SDK is alpha and may have breaking changes. However, OpenShell itself is alpha (badge on README), the BFF already pins a specific proto commit, and the SDK follows semver once tagged. Pinning the SDK version is no riskier than pinning the proto version.
Use the SDK only for new features. Creates a split where some handlers use the SDK and others use raw gRPC. Acceptable as an intermediate state (Phase 1-3) but not as a permanent architecture.
Agent Investigation
Verified against openshell-dashboard at 8bf7a76 and NVIDIA/OpenShell main (Aug 11 2026):
backend/internal/gateway/interface.go: 57 methods onInterface.backend/internal/gateway/client.go:grpc.NewClientwithPerRPCCredentials, URL prefix parsing (grpcs://,http://).backend/gen/openshellv1/: regenerated stubs from OpenShell proto. Module pathgithub.com/Gkrumbach07/openshell-dashboard/backend/gen/openshellv1.- SDK at
sdk/go/openshell/v1/:NewClient(Config{Address, Auth, TLS}),Sandboxes()implemented (11 methods), 9 stub sub-clients instub_clients.go. - SDK PR #2702: adds remaining 14 domain clients, OIDC flows, fakes, gateway discovery. 100 files, 1265 tests.
- SDK infrastructure is ~80% complete (13 interfaces, 20 domain types, converters, auth, error model). Only method body wiring is stubbed (195 lines).
Checklist
- Reviewed existing BFF gateway code (57 methods, 53 RPCs)
- Reviewed SDK API surface and implementation status
- Verified proto compatibility between BFF stubs and SDK
- Confirmed SDK #2702 covers all BFF RPC needs
- Identified auth model challenge and proposed solution
- Verified InteractiveSession compatibility for terminal WebSocket bridge
- Verified grpc-go version compatibility (v1.80 + v1.82)
- Confirmed Inference is only blocker (3 methods, pending #2702)
- This is a design proposal tracking incremental migration
Activity
rhuss commentedon Aug 12, 2026
If you find any dragons in the Golang SDK, please let me know (best, open an issue upstream and ping me and @ChristianZaccaria, that would be awesome). Thank you!