From 7b4ad8bfde795845fee1559156b8654505627c9f Mon Sep 17 00:00:00 2001 From: Max Dubrinsky Date: Tue, 4 Aug 2026 13:00:32 -0400 Subject: [PATCH 1/2] fix(nemo-deployments): wrap AsyncEntitiesClient via client_from_platform in OpenShell backend OpenShellDeploymentBackend.init() wrapped the SDK's generated AsyncEntitiesResource directly instead of adapting it with client_from_platform(sdk, AsyncEntitiesClient), as the docker and k8s backends do. NemoEntitiesClient.get() forwards a query_params kwarg the generated resource does not accept, so _load_deployment_config raised TypeError: AsyncEntitiesResource.get_entity_by_name() got an unexpected keyword argument 'query_params' and every first reconcile against an openshell executor failed. Adapt the SDK the same way as the docker backend so the wrapped client accepts query_params. Add a regression test that drives the real contract (a live entities client over a mock transport): a 404 surfaces as NemoEntityNotFoundError, which is only reachable once get_entity_by_name(query_params=...) is accepted. The test fails against the old wrapping with the exact query_params TypeError. Update the openshell conftest to patch client_from_platform instead of the removed AsyncEntitiesResource. Fixes AIRCORE-977 Signed-off-by: Max Dubrinsky --- .../backends/openshell/backend.py | 5 +-- .../tests/unit/backends/openshell/conftest.py | 2 +- .../test_openshell_backend_mocked.py | 31 +++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/backend.py b/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/backend.py index 19b334874b..06682fa959 100644 --- a/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/backend.py +++ b/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/backend.py @@ -58,7 +58,8 @@ from nemo_deployments_plugin.entities import Container, DeploymentConfig from nemo_deployments_plugin.secrets import SecretResolutionError, resolve_deployment_config_secrets from nemo_deployments_plugin.types import DeploymentStatus, Endpoint -from nemo_platform.resources.entities import AsyncEntitiesResource +from nemo_platform_plugin.client.adapter import client_from_platform +from nemo_platform_plugin.entities.client import AsyncEntitiesClient from nemo_platform_plugin.entity_client import NemoEntitiesClient, NemoEntityNotFoundError if TYPE_CHECKING: @@ -154,7 +155,7 @@ class OpenShellDeploymentBackend(DeploymentBackend): def init(self) -> None: _ensure_openshell() self._executor_config = OpenShellExecutorConfig.model_validate(self._config) - self._entities = NemoEntitiesClient(AsyncEntitiesResource(self._sdk)) + self._entities = NemoEntitiesClient(client_from_platform(self._sdk, AsyncEntitiesClient)) # Build the policy once (fail fast on a bad path/shape). The gateway default # policy would not permit the agent's own exec paths, so we always apply one. self._policy = self._build_executor_policy() diff --git a/plugins/nemo-deployments/tests/unit/backends/openshell/conftest.py b/plugins/nemo-deployments/tests/unit/backends/openshell/conftest.py index fc7a442594..3a94d6ee6d 100644 --- a/plugins/nemo-deployments/tests/unit/backends/openshell/conftest.py +++ b/plugins/nemo-deployments/tests/unit/backends/openshell/conftest.py @@ -35,7 +35,7 @@ def openshell_backend( mock_sdk: MagicMock, mock_entities: AsyncMock, mock_stub: MagicMock ) -> Iterator[OpenShellDeploymentBackend]: with ( - patch("nemo_deployments_plugin.backends.openshell.backend.AsyncEntitiesResource"), + patch("nemo_deployments_plugin.backends.openshell.backend.client_from_platform"), patch("nemo_deployments_plugin.backends.openshell.backend.NemoEntitiesClient", return_value=mock_entities), patch("grpc.insecure_channel", return_value=MagicMock()), ): diff --git a/plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_backend_mocked.py b/plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_backend_mocked.py index 67c1092f19..12a6ba6025 100644 --- a/plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_backend_mocked.py +++ b/plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_backend_mocked.py @@ -12,6 +12,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import grpc +import httpx import pytest from nemo_deployments_plugin.backends.labels import ( CONFIG_NAME_LABEL, @@ -35,6 +36,8 @@ from nemo_deployments_plugin.constants import MANAGED_BY_LABEL from nemo_deployments_plugin.entities import Container, ContainerPort, DeploymentConfig, EnvVar from nemo_deployments_plugin.secrets import SecretResolutionError +from nemo_platform import AsyncNeMoPlatform +from nemo_platform_plugin.entity_client import NemoEntityNotFoundError pytest.importorskip("openshell") # platform-restricted extra; skip where not installed (e.g. CI) @@ -126,6 +129,34 @@ def test_registry_contains_openshell() -> None: assert BACKEND_CLASSES["openshell"] is OpenShellDeploymentBackend +async def test_load_deployment_config_wraps_an_entities_client_that_accepts_query_params() -> None: + """init() must adapt the SDK with client_from_platform(AsyncEntitiesClient), not wrap the + raw generated AsyncEntitiesResource (AIRCORE-977). + + NemoEntitiesClient.get() forwards a ``query_params`` kwarg. The generated resource does not + accept it, so wrapping the resource made every first reconcile die with + ``TypeError: ... unexpected keyword argument 'query_params'`` before any request left the box. + Drive the real contract with a live entities client over a mock transport: a 404 must surface + as NemoEntityNotFoundError, which is only reachable once ``get_entity_by_name(query_params=...)`` + is accepted and the request actually goes out. + """ + + def handler(request: httpx.Request) -> httpx.Response: + return httpx.Response(404, json={"detail": "not found"}, request=request) + + sdk = AsyncNeMoPlatform( + base_url="http://entities.test", + workspace="default", + http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)), + ) + with patch("grpc.insecure_channel", return_value=MagicMock()): + backend = OpenShellDeploymentBackend(sdk, {"gateway_endpoint": "http://127.0.0.1:17670"}) + + # Old (buggy) wrapping raised TypeError about query_params here; the fix reaches the 404. + with pytest.raises(NemoEntityNotFoundError): + await backend._load_deployment_config("default", "missing-config") + + def test_sandbox_name_within_limit_and_deterministic() -> None: name = _sandbox_name("a-long-workspace-name", "a-long-deployment-name") assert name.startswith("nmp-") From 99577290f4ea31715649cb2fe4a47ec779837857 Mon Sep 17 00:00:00 2001 From: Max Dubrinsky Date: Tue, 4 Aug 2026 13:01:03 -0400 Subject: [PATCH 2/2] fix(nemo-deployments): correct broken openshell install hint The MissingBackendDependencyError hint told users to run uv sync --package nemo-deployments-plugin --extra openshell, which narrows the workspace venv and uninstalls the platform (AIRCORE-980). Replace it in both the backend and policy modules with the non-destructive uv pip install "openshell>=0.0.92" "grpcio>=1.78.0" "protobuf>=6.31.1". Signed-off-by: Max Dubrinsky --- .../src/nemo_deployments_plugin/backends/openshell/backend.py | 2 +- .../src/nemo_deployments_plugin/backends/openshell/policy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/backend.py b/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/backend.py index 06682fa959..a2878d052e 100644 --- a/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/backend.py +++ b/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/backend.py @@ -71,7 +71,7 @@ _OPENSHELL_INSTALL_HINT = ( "The 'openshell' package is required for OpenShellDeploymentBackend. " - "Install it with: uv sync --package nemo-deployments-plugin --extra openshell" + 'Install it with: uv pip install "openshell>=0.0.92" "grpcio>=1.78.0" "protobuf>=6.31.1"' ) _SERVE_LOG = "/tmp/nemo-serve.log" diff --git a/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/policy.py b/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/policy.py index ed8dbe1c41..8c138a5bb6 100644 --- a/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/policy.py +++ b/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/policy.py @@ -187,7 +187,7 @@ def generate_sandbox_policy(*, filesystem: SandboxFilesystem, egress: PlatformEg _OPENSHELL_INSTALL_HINT = ( "The 'openshell' package is required to build OpenShell sandbox policies. " - "Install it with: uv sync --package nemo-deployments-plugin --extra openshell" + 'Install it with: uv pip install "openshell>=0.0.92" "grpcio>=1.78.0" "protobuf>=6.31.1"' )