Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions .github/actions/build-policy-wasm/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,6 @@ description: >
runs:
using: composite
steps:
- name: Install OPA
shell: bash
env:
OPA_VERSION: v1.8.0
run: |
curl -fsSL -o /usr/local/bin/opa \
"https://github.com/open-policy-agent/opa/releases/download/${OPA_VERSION}/opa_linux_amd64_static"
chmod +x /usr/local/bin/opa
- name: Build policy WASM
shell: bash
run: ./script/build_policy_wasm.sh
7 changes: 7 additions & 0 deletions docs/auth/authorization/policy-engine.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,23 @@ The embedded PDP uses a WASM-compiled Rego policy engine built into the auth ser

### How It Works

- Rego policy files are compiled to `policy.wasm` with `make build-policy`
- Policy data (role bindings, workspace visibility, endpoint permissions) is served by the auth service
- Data is refreshed on a configurable interval (default: 30 seconds)

In source checkouts, startup with `auth.enabled: true` and
`policy_decision_point_provider: embedded` automatically builds a missing or
stale `policy.wasm` using the pinned OPA version from `script/build_policy_wasm.sh`.
Packaged wheels and container images should include `policy.wasm` at build time.

### Configuration

```yaml
auth:
enabled: true
policy_decision_point_provider: "embedded"
policy_decision_point_base_url: "http://auth:8000"
embedded_pdp_auto_build_wasm: false
policy_data_refresh_interval: 30 # seconds
Comment on lines 38 to 44

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Config example contradicts the documented/default behavior.

This snippet sets embedded_pdp_auto_build_wasm: false, but the section describes auto-build behavior for source checkouts and the code default is true.

Suggested fix
- embedded_pdp_auto_build_wasm: false
+ embedded_pdp_auto_build_wasm: true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```yaml
auth:
enabled: true
policy_decision_point_provider: "embedded"
policy_decision_point_base_url: "http://auth:8000"
embedded_pdp_auto_build_wasm: false
policy_data_refresh_interval: 30 # seconds
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/auth/authorization/policy-engine.mdx` around lines 38 - 44, The
configuration example in the policy-engine.mdx file shows
`embedded_pdp_auto_build_wasm: false`, but this contradicts the section's
description of auto-build behavior and the actual code default of `true`. Update
the example value for `embedded_pdp_auto_build_wasm` from `false` to `true` to
align with the documented default behavior and code implementation.

```

Expand Down
23 changes: 23 additions & 0 deletions docs/auth/deployment/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ NMP_AUTH_ENABLED=true
NMP_AUTH_POLICY_DECISION_POINT_BASE_URL=http://auth:8000
NMP_AUTH_POLICY_DECISION_POINT_PROVIDER=embedded
NMP_AUTH_ADMIN_EMAIL=admin@example.com
NMP_AUTH_EMBEDDED_PDP_AUTO_BUILD_WASM=true
```

Nested keys (e.g., OIDC) use double underscore: `NMP_AUTH_OIDC__ISSUER`, `NMP_AUTH_OIDC__CLIENT_ID`.
Expand All @@ -93,16 +94,38 @@ auth:
enabled: true
policy_decision_point_provider: embedded
policy_decision_point_base_url: "http://localhost:8080"
embedded_pdp_auto_build_wasm: true
admin_email: "admin@example.com"
```

When running from a source checkout, embedded PDP startup automatically builds a
missing or stale `policy.wasm` with the pinned OPA version used by
`make build-policy`. Packaged deployments should include `policy.wasm` at image
or wheel build time; set `embedded_pdp_auto_build_wasm: false` to fail fast if
that artifact is missing.

In offline development environments, provide the pinned OPA binary explicitly:

```bash
OPA_BIN=/path/to/opa_linux_amd64_static uv run nemo services run --host 127.0.0.1 --port 8080
```

Or seed the local cache used by `script/build_policy_wasm.sh`:

```bash
mkdir -p .cache/opa/v1.8.0
cp /path/to/opa_linux_amd64_static .cache/opa/v1.8.0/opa_linux_amd64_static
chmod +x .cache/opa/v1.8.0/opa_linux_amd64_static
```

### Production with embedded PDP

```yaml
auth:
enabled: true
policy_decision_point_base_url: "http://auth:8000"
policy_decision_point_provider: embedded
embedded_pdp_auto_build_wasm: false
policy_data_refresh_interval: 30
admin_email: "platform-admin@company.com"
oidc:
Expand Down
2 changes: 2 additions & 0 deletions docs/set-up/config-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ auth:
propagation_poll_interval_seconds: 1.0
# Allow unsigned JWTs (`alg=none`) for local development/testing. Disabled by default and should not be enabled in production. | default: False
allow_unsigned_jwt: false
# When auth is enabled with the embedded PDP and policy.wasm is missing or stale, build it automatically from a local NeMo Platform source checkout. Packaged deployments should include policy.wasm at build time and can disable this for fail-fast startup. | default: True
embedded_pdp_auto_build_wasm: true
# OIDC configuration for native token validation.
oidc:
# Enable native OIDC token validation. | default: False
Expand Down
9 changes: 9 additions & 0 deletions packages/nmp_common/src/nmp/common/config/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,15 @@ class AuthConfig(create_service_config_class("auth")):
),
)

embedded_pdp_auto_build_wasm: bool = Field(
default=True,
description=(
"When auth is enabled with the embedded PDP and policy.wasm is missing or stale, "
"build it automatically from a local NeMo Platform source checkout. Packaged deployments "
"should include policy.wasm at build time and can disable this for fail-fast startup."
),
)

oidc: OIDCConfig = Field(
default_factory=OIDCConfig,
description="OIDC configuration for native token validation.",
Expand Down
23 changes: 23 additions & 0 deletions packages/nmp_platform_runner/src/nmp/platform_runner/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

logger = logging.getLogger(__name__)
console = Console()
error_console = Console(stderr=True)


def _startup_phase(name: str, t0: float) -> None:
Expand All @@ -52,6 +53,25 @@ def _database_display(db_url: str) -> str:
return db_type


def _is_policy_wasm_error(error: Exception) -> bool:
return (
error.__class__.__name__ == "PolicyWasmError"
and error.__class__.__module__ == "nmp.core.auth.app.embedded_pdp.policy_wasm"
)


def _display_policy_wasm_error(error: Exception) -> None:
error_console.print()
error_console.print(
Panel(
str(error),
title="Embedded Auth Policy WASM Startup Failed",
border_style="red",
expand=False,
)
)


def run_controllers_in_threads(
controller_run_funcs: dict[str, Callable],
stop_signal: threading.Event,
Expand Down Expand Up @@ -163,6 +183,9 @@ def signal_handler(signum: int, _frame: object) -> None:
except KeyboardInterrupt:
logger.info("Received keyboard interrupt, shutting down")
except Exception as error:
if _is_policy_wasm_error(error):
_display_policy_wasm_error(error)
raise SystemExit(1) from None
logger.exception("Fatal error occurred")
raise SystemExit(1) from error
finally:
Expand Down
18 changes: 18 additions & 0 deletions packages/nmp_platform_runner/src/nmp/platform_runner/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,22 @@ async def dispatch(self, request: Request, call_next) -> Response:
return await call_next(request)


def preflight_embedded_auth_policy_wasm(auth_config) -> None:
"""Ensure local embedded auth PDP has a loadable policy.wasm before serving traffic."""
if not auth_config.enabled or auth_config.policy_decision_point_provider != "embedded":
return

try:
from nmp.core.auth.app.embedded_pdp.policy_wasm import ensure_embedded_policy_wasm
except ImportError as exc:
raise RuntimeError(
"Auth is enabled with the embedded PDP, but the nmp-auth package is not installed. "
"Install nmp-auth or set auth.policy_decision_point_provider='opa'."
) from exc
Comment on lines +80 to +86

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Narrow the import fallback to true missing-module cases.

Catching ImportError here also captures transitive import failures inside policy_wasm, then reports the wrong remediation (“package not installed”). Only remap missing nmp.core.auth.app.embedded_pdp.policy_wasm; re-raise other import errors unchanged.

Proposed fix
-    try:
-        from nmp.core.auth.app.embedded_pdp.policy_wasm import ensure_embedded_policy_wasm
-    except ImportError as exc:
+    try:
+        from nmp.core.auth.app.embedded_pdp.policy_wasm import ensure_embedded_policy_wasm
+    except ModuleNotFoundError as exc:
+        if exc.name != "nmp.core.auth.app.embedded_pdp.policy_wasm":
+            raise
         raise RuntimeError(
             "Auth is enabled with the embedded PDP, but the nmp-auth package is not installed. "
             "Install nmp-auth or set auth.policy_decision_point_provider='opa'."
         ) from exc
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nmp_platform_runner/src/nmp/platform_runner/server.py` around lines
80 - 86, The try-except block around the import of ensure_embedded_policy_wasm
from nmp.core.auth.app.embedded_pdp.policy_wasm currently catches all
ImportError exceptions, including transitive import failures inside policy_wasm
that should not be remapped to a "package not installed" message. Modify the
exception handler to distinguish between a missing
nmp.core.auth.app.embedded_pdp.policy_wasm module and other import errors: check
if the caught ImportError's name attribute matches the module being imported (or
check the exception message for nmp.core.auth.app.embedded_pdp.policy_wasm), and
only convert that case to the RuntimeError with the installation guidance; for
all other import errors, re-raise the original exception unchanged to preserve
the actual debugging information.


ensure_embedded_policy_wasm(auto_build=getattr(auth_config, "embedded_pdp_auto_build_wasm", True))


def create_platform_openapi_app() -> FastAPI:
"""Create the platform app used for aggregate OpenAPI generation."""
services = []
Expand Down Expand Up @@ -196,13 +212,15 @@ async def root_handler() -> Response:

def run_server(services: list[Service] | None = None, host: str = "0.0.0.0", port: int = 8080) -> None:
"""Run the platform API server."""
preflight_embedded_auth_policy_wasm(get_auth_config())
app = create_app(services or [])
setup_fastapi_instrumentations(app)
uvicorn.run(app, host=host, port=port, log_config=None)


def run_server_with_reload(app_factory: str, host: str = "0.0.0.0", port: int = 8080) -> None:
"""Run the platform API server with uvicorn reload enabled."""
preflight_embedded_auth_policy_wasm(get_auth_config())
reload_dirs = [
"packages/nmp_platform/src",
"services/core",
Expand Down
38 changes: 38 additions & 0 deletions packages/nmp_platform_runner/tests/test_run_policy_wasm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

from nmp.platform_runner import run
from rich.console import Console


class PolicyWasmError(Exception):
pass


PolicyWasmError.__module__ = "nmp.core.auth.app.embedded_pdp.policy_wasm"


def test_policy_wasm_error_is_expected_startup_error():
assert run._is_policy_wasm_error(PolicyWasmError("boom"))
assert not run._is_policy_wasm_error(RuntimeError("boom"))


def test_policy_wasm_error_renders_as_panel(tmp_path, monkeypatch):
stderr = tmp_path / "stderr.txt"
console = Console(file=stderr.open("w"), force_terminal=False, width=100)
monkeypatch.setattr(run, "error_console", console)

run._display_policy_wasm_error(
PolicyWasmError(
"Failed to build embedded auth PDP policy.wasm.\n\n"
"Command:\n"
" script/build_policy_wasm.sh\n\n"
"Offline options:\n"
" OPA_BIN=/path/to/opa ./script/build_policy_wasm.sh"
)
)

output = stderr.read_text()
assert "Embedded Auth Policy WASM Startup Failed" in output
assert "script/build_policy_wasm.sh" in output
assert "OPA_BIN=/path/to/opa" in output
55 changes: 55 additions & 0 deletions packages/nmp_platform_runner/tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,61 @@ def fake_create_app(services, controller_run_funcs=None, _http_client=None):
assert captured["controller_run_funcs"] == {"agents-deployment": plugin_controller}


def test_embedded_auth_preflight_invokes_policy_wasm_helper(monkeypatch):
calls: list[bool] = []
auth_cfg = AuthConfig(
enabled=True,
policy_decision_point_provider="embedded",
embedded_pdp_auto_build_wasm=False,
)

from nmp.core.auth.app.embedded_pdp import policy_wasm

monkeypatch.setattr(policy_wasm, "ensure_embedded_policy_wasm", lambda *, auto_build: calls.append(auto_build))

server.preflight_embedded_auth_policy_wasm(auth_cfg)

assert calls == [False]


@pytest.mark.parametrize(
"auth_cfg",
[
AuthConfig(enabled=False, policy_decision_point_provider="embedded"),
AuthConfig(enabled=True, policy_decision_point_provider="opa"),
],
)
def test_embedded_auth_preflight_skips_when_not_needed(auth_cfg, monkeypatch):
calls: list[bool] = []

from nmp.core.auth.app.embedded_pdp import policy_wasm

monkeypatch.setattr(policy_wasm, "ensure_embedded_policy_wasm", lambda *, auto_build: calls.append(auto_build))

server.preflight_embedded_auth_policy_wasm(auth_cfg)

assert calls == []


def test_run_server_runs_embedded_auth_preflight():
auth_cfg = _make_auth_config(enabled=True)
calls: list[AuthConfig] = []
with (
patch("nmp.platform_runner.server.get_auth_config", return_value=auth_cfg),
patch(
"nmp.platform_runner.server.preflight_embedded_auth_policy_wasm", side_effect=lambda cfg: calls.append(cfg)
),
patch("nmp.platform_runner.server.create_app", return_value=FastAPI()) as create_app,
patch("nmp.platform_runner.server.setup_fastapi_instrumentations"),
patch("nmp.platform_runner.server.uvicorn.run") as uvicorn_run,
):
server.run_server(services=[], host="127.0.0.1", port=9999)

assert calls == [auth_cfg]
create_app.assert_called_once_with([])
uvicorn_run.assert_called_once()


def test_create_default_app_raises_for_unknown_service_from_env(monkeypatch):
monkeypatch.setattr(server, "_obs_initialized", True)
monkeypatch.setenv("NMP_SERVICES", "missing-service")
Expand Down
Loading
Loading