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..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 @@ -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: @@ -70,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" @@ -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/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"' ) 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-")