diff --git a/README.md b/README.md index ba1ccf9859..4003b25937 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,43 @@ The `openshell` package on PyPI provides the Python SDK only. It does not instal uv add openshell ``` +Curated SDK methods cover common sandbox workflows. For gateway RPCs that do +not yet have a curated wrapper, `SandboxClient.raw` exposes the complete +generated OpenShell service over the same authenticated channel. The public +wire types are available from `openshell.raw`: + +```python +import os + +from openshell import SandboxClient, raw + +with SandboxClient.from_active_cluster() as client: + response = client.raw.UpdateProvider( + raw.openshell_pb2.UpdateProviderRequest( + workspace="my-workspace", + provider=raw.datamodel_pb2.Provider( + metadata=raw.datamodel_pb2.ObjectMeta( + name="user-token", + workspace="my-workspace", + ), + credentials={"TOKEN": os.environ["USER_TOKEN"]}, + ), + ), + timeout=30, + ) +``` + +The raw layer returns protobuf messages verbatim. Prefer curated methods when +available, pass an explicit timeout to raw calls, and keep the owning +`SandboxClient` open for the lifetime of calls or streams. Exposing an RPC does +not grant permission to invoke it; the gateway applies the same authentication +and authorization checks to curated and raw calls. + +`SandboxClient.channel` exposes the same authenticated channel when an +additional generated service client is needed, for example +`raw.InferenceStub(client.channel)`. The `SandboxClient` continues to own that +channel and its lifecycle. + **Helm chart:** > **Experimental** — the Kubernetes deployment path is under active development. Expect rough edges and breaking changes. diff --git a/python/openshell/raw.py b/python/openshell/raw.py new file mode 100644 index 0000000000..551c52c997 --- /dev/null +++ b/python/openshell/raw.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Generated OpenShell gRPC surface for advanced SDK use. + +The modules exported here contain the uncurated protobuf wire types. Prefer the +high-level SDK where it has a suitable method; use this module with +``SandboxClient.raw`` when the gateway RPC has not received a curated wrapper. +""" + +from ._proto import ( + datamodel_pb2, + inference_pb2, + openshell_pb2, + options_pb2, + sandbox_pb2, +) +from ._proto.inference_pb2_grpc import InferenceStub +from ._proto.openshell_pb2_grpc import OpenShellStub + +__all__ = [ + "InferenceStub", + "OpenShellStub", + "datamodel_pb2", + "inference_pb2", + "openshell_pb2", + "options_pb2", + "sandbox_pb2", +] diff --git a/python/openshell/sandbox.py b/python/openshell/sandbox.py index 94f3c57def..154f4e7fa0 100644 --- a/python/openshell/sandbox.py +++ b/python/openshell/sandbox.py @@ -572,6 +572,28 @@ def __init__( ) self._stub = openshell_pb2_grpc.OpenShellStub(self._channel) + @property + def raw(self) -> openshell_pb2_grpc.OpenShellStub: + """Generated client for every OpenShell gateway RPC. + + This is the uncurated protobuf API. Request and response types are + available from :mod:`openshell.raw`. Calls reuse this client's channel, + including its TLS and bearer-token configuration. Unless overridden by + the caller, generated methods do not inherit this client's default + timeout. + """ + return self._stub + + @property + def channel(self) -> grpc.Channel: + """Authenticated channel for constructing additional generated clients. + + The channel is owned by this client and closes when :meth:`close` is + called. Generated service clients are available from + :mod:`openshell.raw`. + """ + return self._channel + @classmethod def from_active_cluster( cls, diff --git a/python/openshell/sandbox_test.py b/python/openshell/sandbox_test.py index d70aae49cc..b0c797b204 100644 --- a/python/openshell/sandbox_test.py +++ b/python/openshell/sandbox_test.py @@ -17,6 +17,7 @@ import pytest import openshell.sandbox as sandbox_module +from openshell import raw from openshell._proto import openshell_pb2 from openshell.sandbox import ( _OIDC_TOKEN_EXPIRY_GRACE_SECONDS, @@ -437,6 +438,47 @@ def _client_with_fake_stub(stub: object) -> SandboxClient: return client +def test_raw_exposes_the_authenticated_generated_stub() -> None: + stub = object() + client = _client_with_fake_stub(stub) + + assert client.raw is stub + + +def test_raw_stub_covers_every_gateway_rpc() -> None: + client = SandboxClient("127.0.0.1:1") + try: + service = raw.openshell_pb2.DESCRIPTOR.services_by_name["OpenShell"] + missing = [ + method.name + for method in service.methods + if not hasattr(client.raw, method.name) + ] + assert missing == [] + finally: + client.close() + + +def test_channel_exposes_the_authenticated_transport() -> None: + channel = object() + client = _client_with_fake_stub(object()) + client._channel = cast("Any", channel) + + assert client.channel is channel + + +def test_raw_exports_gateway_wire_types() -> None: + request = raw.openshell_pb2.UpdateProviderRequest(workspace="example") + provider = raw.datamodel_pb2.Provider(type="example") + config_request = raw.sandbox_pb2.GetSandboxConfigRequest() + + assert request.workspace == "example" + assert provider.type == "example" + assert config_request is not None + assert raw.InferenceStub is not None + assert raw.OpenShellStub is not None + + def test_exec_sends_stdin_payload() -> None: stub = _FakeStub() client = _client_with_fake_stub(stub)