From 8c97ef3a460f52b7cc0816fa17931c7f6feec9fa Mon Sep 17 00:00:00 2001 From: Ryan Sadler <267728323+ironcommit@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:30:23 -0700 Subject: [PATCH] docs(auth): restructure auth docs into Fern navigation and fix rendering issues Add the Access Control section to the Fern docs navigation (latest.yml), covering authentication, authorization, deployment, and security model pages. Fix permissions-reference table rendering by replacing escaped pipe characters (`\\|`) with HTML entities (`|`) and `` tags so permission patterns display correctly in MDX. Update cross-document links from relative `.md` paths to absolute Fern URL paths. Improve roles-and-permissions with a Mermaid flowchart for the role hierarchy. Refactor security-model and concepts pages for clarity and accuracy. Add docs-watch.mjs, a file-system watcher that triggers Fern live-reload when any file outside `docs/fern/` changes, and wire it up as `make docs-watch`. Extend auth-tools.py with a `generate-docs` command that regenerates the permissions reference from the auth configuration. Signed-off-by: Ryan Sadler <267728323+ironcommit@users.noreply.github.com> --- Makefile | 4 + docs/AGENTS.md | 3 + docs/auth/authorization/index.mdx | 2 +- .../authorization/permissions-reference.mdx | 86 +++++++------- .../authorization/roles-and-permissions.mdx | 31 +++-- docs/auth/concepts.mdx | 68 ++++++----- docs/auth/deployment/configuration.mdx | 2 +- docs/auth/security-model.mdx | 59 ++++------ docs/customizer/tutorials/index.mdx | 10 +- docs/fern/README.md | 5 +- docs/fern/package.json | 1 + docs/fern/scripts/docs-watch.mjs | 106 ++++++++++++++++++ docs/fern/versions/latest.yml | 54 +++++++++ services/core/auth/scripts/auth-tools.py | 35 ++++-- 14 files changed, 326 insertions(+), 140 deletions(-) create mode 100644 docs/fern/scripts/docs-watch.mjs diff --git a/Makefile b/Makefile index 53a348633cc..f967676ea5f 100644 --- a/Makefile +++ b/Makefile @@ -110,6 +110,10 @@ docs-deps: ## Install the Fern docs tooling (docs/fern node deps) docs: ## Start the Fern docs dev server (local preview, prints a localhost URL) cd docs/fern && npm run dev +.PHONY: docs-watch +docs-watch: ## Start Fern docs dev plus a repo-level watcher for docs/** changes + cd docs/fern && npm run watch + .PHONY: docs-check docs-check: ## Validate the Fern docs (fern check + validate-mdx + gated-link check) cd docs/fern && npm run check diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 96d710bc73c..f67976f8f78 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -10,12 +10,15 @@ Run these from the repo root (they wrap `cd docs/fern && npm run …`): |---|---| | `make docs-deps` | Install docs tooling (first run on a machine) | | `make docs` | Local dev server (live preview) | +| `make docs-watch` | Local dev server plus repo-level watcher for `docs/**` changes outside `docs/fern/` | | `make docs-check` | `fern check` + MDX validation + gated-link check (what CI runs) | | `make docs-broken-links` | Report broken links | | `make docs-fix-links` | Auto-delink references into gated pages | Local preview and the published site read the **same** `docs/fern/versions/latest.yml`, so what you see locally is what ships. +Use `make docs` when you are only editing `docs/fern/` config. Use `make docs-watch` when you are editing page content elsewhere under `docs/`, since it restarts the Fern dev server when repo-level docs files change outside `docs/fern/`. + ## Rules that bite if you miss them - **Navigation is the build.** Fern only builds pages listed in `docs/fern/versions/latest.yml`. A `.mdx` not in the nav is **not built** (404, not indexed) — that is how unready features are gated. Do **not** use `hidden: true` for gating (it still builds/serves the page). diff --git a/docs/auth/authorization/index.mdx b/docs/auth/authorization/index.mdx index 2ca4ead1016..0aa7516d4c9 100644 --- a/docs/auth/authorization/index.mdx +++ b/docs/auth/authorization/index.mdx @@ -38,7 +38,7 @@ Add users to workspaces, assign roles, manage members. Token-level scope model and two-layer authorization. - + Complete list of all permissions with role assignments. diff --git a/docs/auth/authorization/permissions-reference.mdx b/docs/auth/authorization/permissions-reference.mdx index bfde3e70c68..3c76966026a 100644 --- a/docs/auth/authorization/permissions-reference.mdx +++ b/docs/auth/authorization/permissions-reference.mdx @@ -2,12 +2,12 @@ title: "Permissions Reference" description: "" --- -(permissions-reference)= +{/* This page is generated from the auth configuration. Regenerate it with `uv run python services/core/auth/scripts/auth-tools.py generate-docs`. */} -Complete reference of all permissions across the NeMo Platform APIs. Each permission controls access to a specific operation within an individual API. Permissions are assigned to users through [roles](roles-and-permissions.md). +Complete reference of all permissions across the NeMo Platform APIs. Each permission controls access to a specific operation within an individual API. Permissions are assigned to users through [roles](/documentation/access-control/authorization/roles-and-permissions). -For token-level access restrictions, see [API Scopes](api-scopes.md). For the RBAC model, see [Authorization Concepts](../concepts.md). +For token-level access restrictions, see [API Scopes](/documentation/access-control/authorization/api-scopes). For the RBAC model, see [Authorization Concepts](/documentation/access-control/concepts). @@ -18,79 +18,79 @@ PlatformAdmin is omitted — it bypasses permission checks entirely at the polic | Permission | Description | Viewer | Editor | Admin | |------------|-------------|:------:|:------:|:-----:| -| `entities.(read \\| create \\| update \\| delete)` | Read, create, update, delete entities | | | | +| entities.(read | create | update | delete) | Read, create, update, delete entities | | | | ## Files API | Permission | Description | Viewer | Editor | Admin | |------------|-------------|:------:|:------:|:-----:| -| `filesets.(read \\| list)` | Read, list files | ✓ | ✓ | ✓ | -| `filesets.(create \\| update \\| delete)` | Create, update, delete files | | ✓ | ✓ | +| filesets.(read | list) | Read, list files | ✓ | ✓ | ✓ | +| filesets.(create | update | delete) | Create, update, delete files | | ✓ | ✓ | ## Guardrails API | Permission | Description | Viewer | Editor | Admin | |------------|-------------|:------:|:------:|:-----:| | `guardrails.checks.exec` | Execute guardrail checks | | ✓ | ✓ | -| `guardrails.configs.(read \\| list)` | Read, list guardrails configs | ✓ | ✓ | ✓ | -| `guardrails.configs.(create \\| update \\| delete)` | Create, update, delete guardrails configs | | ✓ | ✓ | +| guardrails.configs.(read | list) | Read, list guardrails configs | ✓ | ✓ | ✓ | +| guardrails.configs.(create | update | delete) | Create, update, delete guardrails configs | | ✓ | ✓ | ## IAM API | Permission | Description | Viewer | Editor | Admin | |------------|-------------|:------:|:------:|:-----:| -| `iam.(read \\| list \\| create \\| delete)` | Read, list, create, delete iam | | | ✓ | +| iam.(read | list | create | delete) | Read, list, create, delete iam | | | ✓ | | `iam.bundle.read` | Download OPA authorization bundle (external OPA / advanced ops) | | | | ## Inference API | Permission | Description | Viewer | Editor | Admin | |------------|-------------|:------:|:------:|:-----:| -| `inference.deployment-configs.(read \\| list)` | Read, list inference deployment-configs | ✓ | ✓ | ✓ | -| `inference.deployment-configs.(create \\| delete)` | Create, delete inference deployment-configs | | ✓ | ✓ | -| `inference.deployments.(read \\| list)` | Read, list inference deployments | ✓ | ✓ | ✓ | -| `inference.deployments.(create \\| update \\| delete)` | Create, update, delete inference deployments | | ✓ | ✓ | +| inference.deployment-configs.(read | list) | Read, list inference deployment-configs | ✓ | ✓ | ✓ | +| inference.deployment-configs.(create | delete) | Create, delete inference deployment-configs | | ✓ | ✓ | +| inference.deployments.(read | list) | Read, list inference deployments | ✓ | ✓ | ✓ | +| inference.deployments.(create | update | delete) | Create, update, delete inference deployments | | ✓ | ✓ | | `inference.gateway.model.exec` | Execute model gateway inference | ✓ | ✓ | ✓ | | `inference.gateway.openai.exec` | Execute OpenAI-compatible gateway inference | ✓ | ✓ | ✓ | | `inference.gateway.provider.exec` | Execute provider gateway inference | ✓ | ✓ | ✓ | -| `inference.providers.(read \\| list)` | Read, list inference providers | ✓ | ✓ | ✓ | -| `inference.providers.(create \\| update \\| delete)` | Create, update, delete inference providers | | ✓ | ✓ | -| `inference.virtual-models.(read \\| list)` | Read, list inference virtual-models | ✓ | ✓ | ✓ | -| `inference.virtual-models.(create \\| update \\| delete)` | Create, update, delete inference virtual-models | | ✓ | ✓ | +| inference.providers.(read | list) | Read, list inference providers | ✓ | ✓ | ✓ | +| inference.providers.(create | update | delete) | Create, update, delete inference providers | | ✓ | ✓ | +| inference.virtual-models.(read | list) | Read, list inference virtual-models | ✓ | ✓ | ✓ | +| inference.virtual-models.(create | update | delete) | Create, update, delete inference virtual-models | | ✓ | ✓ | ## Intake API | Permission | Description | Viewer | Editor | Admin | |------------|-------------|:------:|:------:|:-----:| -| `intake.annotations.(read \\| list)` | Read, list intake annotations | ✓ | ✓ | ✓ | -| `intake.annotations.(create \\| delete)` | Create, delete intake annotations | | ✓ | ✓ | -| `intake.evaluator-results.(read \\| list)` | Read, list intake evaluator-results | ✓ | ✓ | ✓ | +| intake.annotations.(read | list) | Read, list intake annotations | ✓ | ✓ | ✓ | +| intake.annotations.(create | delete) | Create, delete intake annotations | | ✓ | ✓ | +| intake.evaluator-results.(read | list) | Read, list intake evaluator-results | ✓ | ✓ | ✓ | | `intake.evaluator-results.create` | Create intake evaluator results | | ✓ | ✓ | | `intake.experiment-groups.read` | Read intake experiment groups | ✓ | ✓ | ✓ | -| `intake.experiment-groups.(create \\| update \\| delete)` | Create, update, delete intake experiment-groups | | ✓ | ✓ | +| intake.experiment-groups.(create | update | delete) | Create, update, delete intake experiment-groups | | ✓ | ✓ | | `intake.experiments.read` | Read intake experiments | ✓ | ✓ | ✓ | -| `intake.experiments.(create \\| update \\| delete)` | Create, update, delete intake experiments | | ✓ | ✓ | +| intake.experiments.(create | update | delete) | Create, update, delete intake experiments | | ✓ | ✓ | | `intake.ingest.create` | Ingest traces into intake | | ✓ | ✓ | -| `intake.spans.(read \\| list)` | Read, list intake spans | ✓ | ✓ | ✓ | +| intake.spans.(read | list) | Read, list intake spans | ✓ | ✓ | ✓ | | `intake.traces.read` | Read intake traces | ✓ | ✓ | ✓ | ## Jobs API | Permission | Description | Viewer | Editor | Admin | |------------|-------------|:------:|:------:|:-----:| -| `jobs.(read \\| list)` | Read, list jobs | ✓ | ✓ | ✓ | -| `jobs.(create \\| update \\| delete \\| cancel)` | Create, update, delete, cancel jobs | | ✓ | ✓ | +| jobs.(read | list) | Read, list jobs | ✓ | ✓ | ✓ | +| jobs.(create | update | delete | cancel) | Create, update, delete, cancel jobs | | ✓ | ✓ | ## Models API | Permission | Description | Viewer | Editor | Admin | |------------|-------------|:------:|:------:|:-----:| -| `models.(read \\| list)` | Read, list models | ✓ | ✓ | ✓ | -| `models.(create \\| update \\| delete)` | Create, update, delete models | | ✓ | ✓ | -| `models.adapters.(read \\| list)` | Read, list models adapters | ✓ | ✓ | ✓ | -| `models.adapters.(create \\| update \\| delete)` | Create, update, delete models adapters | | ✓ | ✓ | +| models.(read | list) | Read, list models | ✓ | ✓ | ✓ | +| models.(create | update | delete) | Create, update, delete models | | ✓ | ✓ | +| models.adapters.(read | list) | Read, list models adapters | ✓ | ✓ | ✓ | +| models.adapters.(create | update | delete) | Create, update, delete models adapters | | ✓ | ✓ | | `models.prompts.read` | Read model prompts | ✓ | ✓ | ✓ | -| `models.prompts.(create \\| update \\| delete)` | Create, update, delete models prompts | | ✓ | ✓ | +| models.prompts.(create | update | delete) | Create, update, delete models prompts | | ✓ | ✓ | | `models.prompts.list` | List model prompts | | | | | `models.tool-call-plugin.set` | Whether this user can set tool_call_plugin on Models or Deployment Configs *(policy-enforced)* | | | ✓ | | `models.trust-remote-code.set` | Whether this user can set trust_remote_code on Models *(policy-enforced)* | | | ✓ | @@ -105,35 +105,35 @@ PlatformAdmin is omitted — it bypasses permission checks entirely at the polic | Permission | Description | Viewer | Editor | Admin | |------------|-------------|:------:|:------:|:-----:| -| `projects.(read \\| list)` | Read, list projects | ✓ | ✓ | ✓ | -| `projects.(create \\| update \\| delete)` | Create, update, delete projects | | ✓ | ✓ | +| projects.(read | list) | Read, list projects | ✓ | ✓ | ✓ | +| projects.(create | update | delete) | Create, update, delete projects | | ✓ | ✓ | ## Safe Synthesizer API | Permission | Description | Viewer | Editor | Admin | |------------|-------------|:------:|:------:|:-----:| -| `safe-synthesizer.jobs.(read \\| list \\| create \\| delete \\| cancel)` | Read, list, create, delete, cancel safe synthesizer jobs | | | | +| safe-synthesizer.jobs.(read | list | create | delete | cancel) | Read, list, create, delete, cancel safe synthesizer jobs | | | | ## Secrets API | Permission | Description | Viewer | Editor | Admin | |------------|-------------|:------:|:------:|:-----:| -| `secrets.(read \\| list)` | Read, list secrets | ✓ | ✓ | ✓ | -| `secrets.(create \\| update \\| delete)` | Create, update, delete secrets | | ✓ | ✓ | -| `secrets.(access \\| rotate)` | Access, rotate secrets | | | | +| secrets.(read | list) | Read, list secrets | ✓ | ✓ | ✓ | +| secrets.(create | update | delete) | Create, update, delete secrets | | ✓ | ✓ | +| secrets.(access | rotate) | Access, rotate secrets | | | | ## Workspaces API | Permission | Description | Viewer | Editor | Admin | |------------|-------------|:------:|:------:|:-----:| -| `workspaces.(read \\| list)` | Read, list workspaces | ✓ | ✓ | ✓ | -| `workspaces.(update \\| delete)` | Update, delete workspaces | | ✓ | ✓ | -| `workspaces.members.(list \\| create \\| update \\| delete)` | List, create, update, delete workspaces members | | | ✓ | +| workspaces.(read | list) | Read, list workspaces | ✓ | ✓ | ✓ | +| workspaces.(update | delete) | Update, delete workspaces | | ✓ | ✓ | +| workspaces.members.(list | create | update | delete) | List, create, update, delete workspaces members | | | ✓ | | `workspaces.members.read` | Read workspace member details | | | | ## Related -- [Roles & Permissions](roles-and-permissions.md) — Role descriptions and hierarchy. -- [API Scopes](api-scopes.md) — Token-level scope restrictions. -- [Authorization Concepts](../concepts.md) — Workspaces, roles, bindings, and the RBAC model. -- [Security Model](../security-model.md) — Trust boundaries and authorization layers. +- [Roles & Permissions](/documentation/access-control/authorization/roles-and-permissions) — Role descriptions and hierarchy. +- [API Scopes](/documentation/access-control/authorization/api-scopes) — Token-level scope restrictions. +- [Authorization Concepts](/documentation/access-control/concepts) — Workspaces, roles, bindings, and the RBAC model. +- [Security Model](/documentation/access-control/security-model) — Trust boundaries and authorization layers. diff --git a/docs/auth/authorization/roles-and-permissions.mdx b/docs/auth/authorization/roles-and-permissions.mdx index 5caab130b79..20c0ceffaef 100644 --- a/docs/auth/authorization/roles-and-permissions.mdx +++ b/docs/auth/authorization/roles-and-permissions.mdx @@ -41,19 +41,32 @@ NeMo Platform provides four predefined roles, each designed for a specific user Each role includes all permissions of the roles below it: -```text -PlatformAdmin - ▲ - Admin (+manage_members, +change_visibility) - ▲ - Editor (+create, +update, +delete, +cancel) - ▲ - Viewer (list, read, inference) +```mermaid +flowchart BT + Viewer["Viewer ++list ++read ++inference"] + Editor["Editor ++create ++update ++delete ++cancel"] + Admin["Admin ++manage members ++change visibility"] + PlatformAdmin["PlatformAdmin +all workspaces +all operations"] + + Viewer --> Editor + Editor --> Admin + Admin --> PlatformAdmin ``` ## Permission Matrix -Rows are operations; columns are roles. A checkmark indicates the role has permission. +Rows are operations; columns are roles. Read the hierarchy above first: each role inherits everything below it, and the tables call out the points where additional privileges appear. ### Workspace Operations diff --git a/docs/auth/concepts.mdx b/docs/auth/concepts.mdx index 0930bc4b47a..ecd4f7b70eb 100644 --- a/docs/auth/concepts.mdx +++ b/docs/auth/concepts.mdx @@ -33,11 +33,16 @@ Workspaces can have different access levels based on role bindings: Each principal can have different roles in different workspaces: -```text -alice@company.com: - - Admin in workspace "team-ml" - - Editor in workspace "shared-datasets" - - Viewer in workspace "prod-models" +```mermaid +flowchart TB + Alice["alice@company.com"] + TeamML["team-ml
Admin"] + Shared["shared-datasets
Editor"] + Prod["prod-models
Viewer"] + + Alice --> TeamML + Alice --> Shared + Alice --> Prod ``` ### Wildcard Principal @@ -46,10 +51,14 @@ The special principal `*` (asterisk) represents **all authenticated users**. Gra This is the mechanism for creating shared workspaces: -```text -Workspace: "shared-datasets" -├── Role Binding: principal="*", role="Viewer" # Everyone can read -└── Role Binding: principal="alice@company.com", role="Admin" # Alice manages +```mermaid +flowchart TB + Workspace["Workspace: shared-datasets"] + Everyone["Role Binding
principal=*
role=Viewer"] + AliceAdmin["Role Binding
principal=alice@company.com
role=Admin"] + + Workspace --> Everyone + Workspace --> AliceAdmin ``` When both a wildcard binding and an explicit binding exist for a user, the highest role wins. @@ -60,14 +69,16 @@ When both a wildcard binding and an explicit binding exist for a user, the highe The predefined roles form a hierarchy — each role includes all permissions of the roles below it: -```text -PlatformAdmin (all operations across all workspaces) - ▲ - Admin (+ manage members, change visibility) - ▲ - Editor (+ create, update, delete resources) - ▲ - Viewer (list, read, run inference) +```mermaid +flowchart BT + Viewer["Viewer
list, read, run inference"] + Editor["Editor
create, update, delete resources"] + Admin["Admin
manage members, change visibility"] + PlatformAdmin["PlatformAdmin
all operations across all workspaces"] + + Viewer --> Editor + Editor --> Admin + Admin --> PlatformAdmin ``` Custom roles can be defined at deployment time with arbitrary permission sets — they do not need to follow this hierarchy. For the complete permission matrix, see [Roles & Permissions](/documentation/access-control/authorization/roles-and-permissions). @@ -118,19 +129,16 @@ A role binding contains: - **Workspace**: The workspace where access is granted (e.g., `team-ml`) - **Role**: The role being assigned (e.g., `Editor`) -```text -Workspace: "team-ml-research" -├── Role Binding 1 -│ ├── Principal: "alice@company.com" -│ └── Role: "Admin" -│ -├── Role Binding 2 -│ ├── Principal: "bob@company.com" -│ └── Role: "Editor" -│ -└── Role Binding 3 - ├── Principal: "charlie@company.com" - └── Role: "Viewer" +```mermaid +flowchart TB + WorkspaceRB["Workspace: team-ml-research"] + RB1["Role Binding 1
alice@company.com
Admin"] + RB2["Role Binding 2
bob@company.com
Editor"] + RB3["Role Binding 3
charlie@company.com
Viewer"] + + WorkspaceRB --> RB1 + WorkspaceRB --> RB2 + WorkspaceRB --> RB3 ``` ## Automatic Role Assignment diff --git a/docs/auth/deployment/configuration.mdx b/docs/auth/deployment/configuration.mdx index a3c9e881ed2..1e36421a290 100644 --- a/docs/auth/deployment/configuration.mdx +++ b/docs/auth/deployment/configuration.mdx @@ -37,7 +37,7 @@ auth: admin_email: "your-admin@company.com" ``` -For a complete reference of all `auth` fields and their defaults, see the [platform configuration reference](/documentation/reference/configuration-reference). Auth-related values are found under `platformConfig.auth` in the values file. +This page covers the auth-specific configuration fields you need to enable and operate authorization. Auth-related values are found under `platformConfig.auth` in the values file. For OIDC-specific fields (`auth.oidc`), see [OIDC Setup](/documentation/access-control/authentication/oidc-setup). diff --git a/docs/auth/security-model.mdx b/docs/auth/security-model.mdx index a16aed29932..6ec307c2e1c 100644 --- a/docs/auth/security-model.mdx +++ b/docs/auth/security-model.mdx @@ -8,46 +8,25 @@ For hands-on setup, see [Auth Configuration](/documentation/access-control/deplo ## Architecture Overview -```text - ┌─────┐ - │ IdP │ - └──▲──┘ - │ - Validate credentials - │ - ┌────────┐ │ - │ Client │──── Bearer token ────┐ │ - └────────┘ │ │ - │ │ -┌────────────────────────────────┼───────┼───────────────────────────────────┐ -│ Trust Boundary │ │ -│ │ │ │ -│ ┌───────────▼───┐ │ │ -│ │ Gateway │───┘ │ -│ │ (e.g. Envoy)│ │ -│ │ │── X-NMP-Principal-* ──►┌────────────┐ │ -│ └───────┬───────┘ │ PDP │ │ -│ │ │ (Embedded │ │ -│ │◄──── Allow/Deny ───────────────│ or OPA) │ │ -│ │ └─────┬──────┘ │ -│ │ │ │ -│ X-NMP-Principal-* Fetches policy │ -│ X-NMP-Authorized + role data │ -│ │ │ │ -│ ▼ ▼ │ -│ ┌─────────┐ Check permissions ┌──────────────┐ │ -│ │ Service │◄───────────────────────►│ │ │ -│ │ │ (if not pre-authorized) │ Auth Service │ │ -│ └────┬────┘ │ │ │ -│ │ └──────────────┘ │ -│ Forward X-NMP-Principal-* │ -│ │ │ -│ ▼ │ -│ ┌──────────┐ │ -│ │Downstream│ (Jobs, inference, other services) │ -│ │ Service │ │ -│ └──────────┘ │ -└────────────────────────────────────────────────────────────────────────────┘ +```mermaid +sequenceDiagram + participant IdP + participant Client + participant Gateway as Gateway / First Service + participant PDP as PDP (Embedded or OPA) + participant Auth as Auth Service + participant Downstream as Downstream Services + + Note over Gateway,Downstream: Trust Boundary + + Client->>IdP: Validate credentials + IdP-->>Client: JWT + Client->>Gateway: Authorization: Bearer token + Gateway->>PDP: Check authorization + Auth-->>PDP: Policy + role data + PDP-->>Gateway: Allow / deny + Gateway->>Downstream: Forward trusted X-NMP-Principal-* headers + Gateway->>Downstream: Forward X-NMP-Authorized: true ``` The request flow: diff --git a/docs/customizer/tutorials/index.mdx b/docs/customizer/tutorials/index.mdx index e0b88e00608..6214b79930d 100644 --- a/docs/customizer/tutorials/index.mdx +++ b/docs/customizer/tutorials/index.mdx @@ -44,28 +44,28 @@ Learn how to format datasets for different model types. - + Learn how to perform supervised fine-tuning with LoRA adapters using custom data. nemo-customizer - + Learn how to perform supervised fine-tuning using custom data by modifying the all training parameters. nemo-customizer - + Learn how to compress a larger teacher model into a smaller student model using knowledge distillation. nemo-customizer knowledge-distillation - + Learn how to fine-tune embedding models using LoRA merged training for improved question-answering and retrieval tasks. @@ -86,7 +86,7 @@ Learn how to check job metrics using MLflow or Weights & Biases. nemo-customizer mlflow wandb - + Learn how to optimize the token-per-GPU throughput for a LoRA optimization job. diff --git a/docs/fern/README.md b/docs/fern/README.md index 0a4499b3c24..0633efe8d42 100644 --- a/docs/fern/README.md +++ b/docs/fern/README.md @@ -8,7 +8,7 @@ This directory holds the Fern **configuration** for the NeMo Platform documentat | --- | --- | | Fern dashboard | https://dashboard.buildwithfern.com (NVIDIA org) | | Contributor/agent guide | [`../AGENTS.md`](../AGENTS.md) | -| Make targets | `make docs`, `docs-check`, `docs-broken-links`, `docs-fix-links`, `docs-preview` (repo root) | +| Make targets | `make docs`, `make docs-watch`, `docs-check`, `docs-broken-links`, `docs-fix-links`, `docs-preview` (repo root) | | CI workflows | [`../../.github/workflows/`](../../.github/workflows/) (`fern-docs-*.yaml`) | | Publish workflow | [`../../.github/workflows/publish-fern-docs.yaml`](../../.github/workflows/publish-fern-docs.yaml) | @@ -21,8 +21,11 @@ make docs-deps # one-time: install docs/fern tooling (needed for MDX validat make docs-login # one-time per machine: Fern CLI auth for the nvidia org make docs-check # validate: fern check + MDX validation + gated-link check (what CI runs) make docs # start local preview (prints a localhost URL) +make docs-watch # start local preview plus a repo-level watcher for docs/** changes ``` +Use `make docs` when you are only editing `docs/fern/` config. Use `make docs-watch` when you are editing page content elsewhere under `docs/`, since it restarts the Fern dev server when repo-level docs files change outside `docs/fern/`. + `package.json` shells out to `npx -y fern-api@latest` for the Fern CLI itself, but `make docs-deps` (`npm ci`) is required once because the MDX validator (`@mdx-js/mdx`) is a local dependency. ## Layout diff --git a/docs/fern/package.json b/docs/fern/package.json index 4b5e4bbfeb8..0b302da6261 100644 --- a/docs/fern/package.json +++ b/docs/fern/package.json @@ -8,6 +8,7 @@ "fix:gated-links": "node scripts/delink-gated.mjs --fix", "broken-links": "npx -y fern-api@latest docs broken-links", "dev": "npx -y fern-api@latest docs dev", + "watch": "node scripts/docs-watch.mjs", "generate": "npx -y fern-api@latest generate --docs", "preview": "npx -y fern-api@latest generate --docs --preview" }, diff --git a/docs/fern/scripts/docs-watch.mjs b/docs/fern/scripts/docs-watch.mjs new file mode 100644 index 00000000000..0f5a814c371 --- /dev/null +++ b/docs/fern/scripts/docs-watch.mjs @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { spawn } from "node:child_process"; +import { watch } from "node:fs"; +import { utimes } from "node:fs/promises"; +import { constants } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const fernDir = path.resolve(scriptDir, ".."); +const docsRoot = path.resolve(fernDir, ".."); +const reloadTrigger = path.join(fernDir, "docs.yml"); +const reloadDebounceMs = 150; +const ignoredPrefix = "fern/"; + +let debounceTimer = null; +let shuttingDown = false; + +function log(message) { + process.stdout.write(`[docs-watch] ${message}\n`); +} + +async function touchReloadTrigger(changedPath) { + const now = new Date(); + await utimes(reloadTrigger, now, now); + log(`triggered Fern reload via docs.yml after change in ${changedPath}`); +} + +function clearPendingReload() { + if (debounceTimer === null) { + return; + } + clearTimeout(debounceTimer); + debounceTimer = null; +} + +function scheduleReload(relativePath) { + clearPendingReload(); + + debounceTimer = setTimeout(() => { + debounceTimer = null; + touchReloadTrigger(relativePath).catch((error) => { + log(`failed to trigger reload: ${error.message}`); + }); + }, reloadDebounceMs); +} + +function shouldIgnore(relativePath) { + const normalized = path.posix.normalize(relativePath.split(path.sep).join("/")); + return normalized.startsWith(ignoredPrefix); +} + +const fern = spawn("npx", ["-y", "fern-api@latest", "docs", "dev"], { + cwd: fernDir, + stdio: "inherit", +}); + +const watcher = watch( + docsRoot, + { recursive: true }, + (_eventType, filename) => { + const relativePath = filename ? filename.toString() : ""; + if (shouldIgnore(relativePath)) { + return; + } + scheduleReload(relativePath); + }, +); + +watcher.on("error", (error) => { + log(`watcher error: ${error.message}`); +}); + +function closeWatcher() { + watcher.close(); +} + +function signalExitCode(signal) { + const signalNumber = constants.signals[signal]; + return signalNumber ? 128 + signalNumber : 1; +} + +function shutdown(signal) { + if (shuttingDown) { + return; + } + shuttingDown = true; + closeWatcher(); + clearPendingReload(); + fern.kill(signal); + process.exit(signalExitCode(signal)); +} + +process.on("SIGINT", () => shutdown("SIGINT")); +process.on("SIGTERM", () => shutdown("SIGTERM")); + +fern.on("exit", (code, signal) => { + closeWatcher(); + if (signal) { + process.kill(process.pid, signal); + return; + } + process.exit(code ?? 0); +}); diff --git a/docs/fern/versions/latest.yml b/docs/fern/versions/latest.yml index cf722a8e09e..c66b488534f 100644 --- a/docs/fern/versions/latest.yml +++ b/docs/fern/versions/latest.yml @@ -23,6 +23,60 @@ navigation: path: ../../get-started/concepts/projects.mdx - page: Workspaces path: ../../get-started/concepts/workspaces.mdx + - section: Access Control + slug: access-control + path: ../../auth/index.mdx + contents: + - page: Overview + path: ../../auth/index.mdx + - page: Concepts + path: ../../auth/concepts.mdx + - section: Authentication + slug: authentication + path: ../../auth/authentication/index.mdx + contents: + - page: OIDC Setup + path: ../../auth/authentication/oidc.mdx + - page: Using Authentication + path: ../../auth/authentication/using-authentication.mdx + - section: Providers + slug: providers + path: ../../auth/authentication/providers/index.mdx + contents: + - page: Azure AD (Entra ID) + path: ../../auth/authentication/providers/azure-ad.mdx + - page: Generic OIDC + path: ../../auth/authentication/providers/generic.mdx + - section: Authorization + slug: authorization + path: ../../auth/authorization/index.mdx + contents: + - page: API Scopes + path: ../../auth/authorization/api-scopes.mdx + - page: Managing Access + path: ../../auth/authorization/managing-access.mdx + - page: Permissions Reference + path: ../../auth/authorization/permissions-reference.mdx + - page: Policy Engine + path: ../../auth/authorization/policy-engine.mdx + - page: Roles and Permissions + path: ../../auth/authorization/roles-and-permissions.mdx + - section: Deployment + slug: deployment + path: ../../auth/deployment/configuration.mdx + contents: + - page: Configuration + path: ../../auth/deployment/configuration.mdx + - page: Credential Propagation + path: ../../auth/deployment/credential-propagation.mdx + - page: Gateway Integration + path: ../../auth/deployment/gateway.mdx + - page: Production Hardening + path: ../../auth/deployment/hardening.mdx + - page: Security Model + path: ../../auth/security-model.mdx + - page: Troubleshooting + path: ../../auth/troubleshooting.mdx - section: Studio path: ../../studio/index.mdx contents: diff --git a/services/core/auth/scripts/auth-tools.py b/services/core/auth/scripts/auth-tools.py index f87a91801c0..e857efff766 100755 --- a/services/core/auth/scripts/auth-tools.py +++ b/services/core/auth/scripts/auth-tools.py @@ -1166,8 +1166,8 @@ def _build_grouped_rows( rows.append(f"| `{perm_name}` | {description} | {role_marks} |") else: actions = [_perm_action(p) for p in group] - actions_str = " \\\\| ".join(actions) - display = f"`{prefix}.({actions_str})`" + actions_str = " | ".join(actions) + display = f"{prefix}.({actions_str})" # Build a combined description from the common prefix desc_parts = prefix.split(".") area_label = ( @@ -1206,18 +1206,23 @@ def _generate_permissions_reference(auth_config: Dict) -> str: lines.append('title: "Permissions Reference"') lines.append('description: ""') lines.append("---") - lines.append("(permissions-reference)=") lines.append("") + lines.append( + "{/* This page is generated from the auth configuration. " + "Regenerate it with `uv run python services/core/auth/scripts/auth-tools.py generate-docs`. */}" + ) lines.append("") lines.append( "Complete reference of all permissions across the NeMo Platform APIs. " "Each permission controls access to a specific operation within an individual API. " - "Permissions are assigned to users through [roles](roles-and-permissions.md)." + "Permissions are assigned to users through " + "[roles](/documentation/access-control/authorization/roles-and-permissions)." ) lines.append("") lines.append( - "For token-level access restrictions, see [API Scopes](api-scopes.md). " - "For the RBAC model, see [Authorization Concepts](../concepts.md)." + "For token-level access restrictions, see " + "[API Scopes](/documentation/access-control/authorization/api-scopes). " + "For the RBAC model, see [Authorization Concepts](/documentation/access-control/concepts)." ) lines.append("") lines.append("") @@ -1241,10 +1246,20 @@ def _generate_permissions_reference(auth_config: Dict) -> str: lines.append("## Related") lines.append("") - lines.append("- [Roles & Permissions](roles-and-permissions.md) — Role descriptions and hierarchy.") - lines.append("- [API Scopes](api-scopes.md) — Token-level scope restrictions.") - lines.append("- [Authorization Concepts](../concepts.md) — Workspaces, roles, bindings, and the RBAC model.") - lines.append("- [Security Model](../security-model.md) — Trust boundaries and authorization layers.") + lines.append( + "- [Roles & Permissions](/documentation/access-control/authorization/roles-and-permissions) " + "— Role descriptions and hierarchy." + ) + lines.append( + "- [API Scopes](/documentation/access-control/authorization/api-scopes) — Token-level scope restrictions." + ) + lines.append( + "- [Authorization Concepts](/documentation/access-control/concepts) " + "— Workspaces, roles, bindings, and the RBAC model." + ) + lines.append( + "- [Security Model](/documentation/access-control/security-model) — Trust boundaries and authorization layers." + ) lines.append("") return "\n".join(lines)