diff --git a/.agents/skills/update-docs/SKILL.md b/.agents/skills/update-docs/SKILL.md index e88247bac..abd3740c8 100644 --- a/.agents/skills/update-docs/SKILL.md +++ b/.agents/skills/update-docs/SKILL.md @@ -10,8 +10,8 @@ Scan recent git history for commits that affect user-facing behavior and draft d ## Prerequisites - You must be in the OpenShell git repository. -- The published docs tree must exist under `fern/pages/`. -- Read `fern/pages/CONTRIBUTING.mdx` before writing any content. It contains the current style guide and formatting rules. +- The published docs tree must exist under `docs/`. +- Read `docs/CONTRIBUTING.mdx` before writing any content. It contains the current style guide and formatting rules. ## When to Use @@ -48,16 +48,16 @@ For each relevant commit, determine which doc page(s) it affects. Use this mappi | Code area | Likely doc page(s) | |---|---| -| `crates/openshell-cli/` (gateway commands) | `fern/pages/sandboxes/manage-gateways.mdx` | -| `crates/openshell-cli/` (sandbox commands) | `fern/pages/sandboxes/manage-sandboxes.mdx` | -| `crates/openshell-cli/` (provider commands) | `fern/pages/sandboxes/manage-providers.mdx` | -| `crates/openshell-cli/` (new top-level command) | May need a new page or `fern/pages/reference/` entry | -| Proxy or policy code | `fern/pages/sandboxes/policies.mdx`, `fern/pages/reference/policy-schema.mdx` | -| Inference code | `fern/pages/inference/configure.mdx` | -| `python/` (SDK changes) | `fern/pages/reference/` or `fern/pages/get-started/quickstart.mdx` | -| `proto/` (API changes) | `fern/pages/reference/` | -| `deploy/` (Dockerfile, Helm) | `fern/pages/sandboxes/manage-gateways.mdx`, `fern/pages/about/architecture.mdx` | -| Community sandbox definitions | `fern/pages/sandboxes/community-sandboxes.mdx` | +| `crates/openshell-cli/` (gateway commands) | `docs/sandboxes/manage-gateways.mdx` | +| `crates/openshell-cli/` (sandbox commands) | `docs/sandboxes/manage-sandboxes.mdx` | +| `crates/openshell-cli/` (provider commands) | `docs/sandboxes/manage-providers.mdx` | +| `crates/openshell-cli/` (new top-level command) | May need a new page or `docs/reference/` entry | +| Proxy or policy code | `docs/sandboxes/policies.mdx`, `docs/reference/policy-schema.mdx` | +| Inference code | `docs/inference/configure.mdx` | +| `python/` (SDK changes) | `docs/reference/` or `docs/get-started/quickstart.mdx` | +| `proto/` (API changes) | `docs/reference/` | +| `deploy/` (Dockerfile, Helm) | `docs/sandboxes/manage-gateways.mdx`, `docs/about/architecture.mdx` | +| Community sandbox definitions | `docs/sandboxes/community-sandboxes.mdx` | If a commit does not map to any existing page but introduces a user-visible concept, flag it as needing a new page. @@ -88,7 +88,7 @@ Identify where the new content should go. Follow the page's existing structure. ## Step 5: Draft the Update -Write the doc update following the rules in `fern/pages/CONTRIBUTING.mdx`. Key reminders: +Write the doc update following the rules in `docs/CONTRIBUTING.mdx`. Key reminders: - **Active voice, present tense, second person.** - **No unnecessary bold.** Reserve bold for UI labels and parameter names. @@ -97,12 +97,12 @@ Write the doc update following the rules in `fern/pages/CONTRIBUTING.mdx`. Key r - **No superlatives.** Say what the feature does, not how great it is. - **Code examples use `shell` language** for copyable commands, with no `$` prompt prefix. - **Use `text` fences** for transcripts, logs, or shell sessions that should not be copied verbatim. -- **Include the SPDX header** if creating a new page. -- **Match existing Fern frontmatter format** if creating a new page, including `sidebar-title`, `keywords`, `tags`, and `position` when they are relevant. Use frontmatter `slug` only for folder-discovered pages or absolute URL overrides. -- **Use `sidebar-title` for short nav labels**. For explicit navbar entries, keep relative `slug` values in `fern/versions/latest.yml` instead of page frontmatter. -- **Keep explicit `page:` entries in `fern/versions/latest.yml`**. Fern still requires them. If the page defines `sidebar-title`, set `page:` to that value. Otherwise set `page:` to the page frontmatter `title`. -- **Use `skip-slug: true` in `fern/versions/latest.yml`** when a child page should live at the parent section path. -- **Use `keywords` as a comma-separated string**. When migrating a page from `docs/`, combine the legacy `topics` and `tags` into `keywords` and preserve the legacy `tags` array when it is still useful. +- **Include the SPDX header as YAML comments in frontmatter** if creating a new page. +- **Match existing Fern frontmatter format** if creating a new page, including `sidebar-title`, `keywords`, and `position` when they are relevant. Use frontmatter `slug` only for folder-discovered pages or absolute URL overrides. +- **Use `sidebar-title` for short nav labels**. For explicit navigation entries, keep relative `slug` values in `docs/index.yml` instead of page frontmatter. +- **Keep explicit `page:` entries in `docs/index.yml`**. Fern still requires them. If the page defines `sidebar-title`, set `page:` to that value. Otherwise set `page:` to the page frontmatter `title`. +- **Use `skip-slug: true` in `docs/index.yml`** when a child page should live at the parent section path. +- **Use `keywords` as a comma-separated string**. - **Do not add a duplicate H1**. Fern renders the page title from frontmatter. - **Always write NVIDIA in all caps.** Wrong: Nvidia, nvidia. - **Always capitalize OpenShell correctly.** Wrong: openshell, Openshell, openShell. @@ -118,8 +118,8 @@ When updating an existing page: When creating a new page: -- Follow the frontmatter template from `fern/pages/CONTRIBUTING.mdx`. -- Add the page to the appropriate section in `fern/versions/latest.yml`. +- Follow the frontmatter template from `docs/CONTRIBUTING.mdx`. +- Add the page to the appropriate section in `docs/index.yml`. ## Step 6: Present the Results @@ -129,8 +129,8 @@ After drafting all updates, present a summary to the user: ## Doc Updates from Commits ### Updated pages -- `fern/pages/sandboxes/manage-gateways.mdx`: Added `--gpu` flag documentation (from commit abc1234). -- `fern/pages/reference/policy-schema.mdx`: Updated network policy schema for new `tls_inspect` field (from commit def5678). +- `docs/sandboxes/manage-gateways.mdx`: Added `--gpu` flag documentation (from commit abc1234). +- `docs/reference/policy-schema.mdx`: Updated network policy schema for new `tls_inspect` field (from commit def5678). ### New pages needed - None (or list any new pages created). @@ -145,13 +145,13 @@ After drafting all updates, present a summary to the user: After making changes, validate the Fern docs locally: ```bash -fern check +mise run docs ``` If a human needs to inspect rendering while iterating, they can also run: ```bash -fern docs dev +mise run docs:serve ``` Check for: @@ -177,4 +177,4 @@ User says: "Catch up the docs for everything merged since v0.2.0." 4. Read the commit diffs and current doc pages. 5. Draft updates following the style guide. 6. Present the summary. -7. Run `fern check` to verify. +7. Run `mise run docs` to verify. diff --git a/.github/workflows/branch-docs.yml b/.github/workflows/branch-docs.yml index d49ee4c99..762d2d1fb 100644 --- a/.github/workflows/branch-docs.yml +++ b/.github/workflows/branch-docs.yml @@ -3,8 +3,12 @@ name: Branch Docs Preview on: pull_request: paths: + - "docs/**" - "fern/**" + - "mise.toml" + - "tasks/docs.toml" - ".github/workflows/branch-docs.yml" + - ".github/workflows/release-tag.yml" permissions: contents: read @@ -30,14 +34,18 @@ jobs: fi - name: Setup Node.js - if: ${{ steps.fern-preview.outputs.enabled == 'true' }} uses: actions/setup-node@v6 with: node-version: "24" - name: Install Fern CLI - if: ${{ steps.fern-preview.outputs.enabled == 'true' }} - run: npm install -g fern-api + run: | + FERN_VERSION=$(node -p "require('./fern/fern.config.json').version") + npm install -g "fern-api@${FERN_VERSION}" + + - name: Validate docs + working-directory: ./fern + run: fern check - name: Generate preview URL if: ${{ steps.fern-preview.outputs.enabled == 'true' }} diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml deleted file mode 100644 index 15c54fbcf..000000000 --- a/.github/workflows/docs-build.yml +++ /dev/null @@ -1,134 +0,0 @@ -name: Docs Build - -on: - push: - branches: [main] - paths: - - "docs/**" - workflow_dispatch: - -defaults: - run: - shell: bash - -env: - MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - runs-on: build-arm64 - container: - image: ghcr.io/nvidia/openshell/ci:latest - credentials: - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install tools - run: mise install - - - name: Build documentation - run: mise run docs:build:strict - - - name: Delete unnecessary files - run: | - find _build -name .doctrees -prune -exec rm -rf {} \; - find _build -name .buildinfo -exec rm {} \; - - - name: Upload HTML - uses: actions/upload-artifact@v4 - with: - name: html-build-artifact - path: _build/docs - if-no-files-found: error - retention-days: 1 - - publish: - if: false # disabled until GitHub Pages is configured - needs: [build] - runs-on: build-arm64 - container: - image: ghcr.io/nvidia/openshell/ci:latest - credentials: - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@v4 - with: - ref: "gh-pages" - - - name: Initialize Git configuration - run: | - git config --global --add safe.directory "$GITHUB_WORKSPACE" - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - - - name: Download artifacts - uses: actions/download-artifact@v4 - with: - name: html-build-artifact - path: ${{ github.ref_name }} - - - name: Copy HTML directories - run: | - ls -asl - for i in `ls -d *` - do - echo "Git adding ${i}" - git add "${i}" - done - - name: Check or create dot-no-jekyll file - - run: | - if [ -f ".nojekyll" ]; then - echo "The dot-no-jekyll file already exists." - exit 0 - fi - touch .nojekyll - git add .nojekyll - - - name: Check or create redirect page - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - resp=$(grep 'http-equiv="refresh"' index.html 2>/dev/null) || true - if [ -n "${resp}" ]; then - echo "The redirect file already exists." - exit 0 - fi - def_branch=$(gh api "repos/${GITHUB_REPOSITORY}" --jq ".default_branch") - html_url=$(gh api "repos/${GITHUB_REPOSITORY}/pages" --jq ".html_url") - echo '' > index.html - echo '' >> index.html - echo ' ' >> index.html - echo ' Redirect to documentation' >> index.html - echo ' ' >> index.html - echo ' ' >> index.html - echo ' ' >> index.html - echo ' ' >> index.html - echo ' ' >> index.html - echo ' ' >> index.html - echo '

Please follow the link to the ' >> index.html - echo ${def_branch}' branch documentation.

' >> index.html - echo ' ' >> index.html - echo '' >> index.html - git add index.html - - - name: Commit changes to the GitHub Pages branch - run: | - git status - if git commit -m 'Pushing changes to GitHub Pages.'; then - git push -f - else - echo "Nothing changed." - fi diff --git a/.github/workflows/docs-preview-pr.yml b/.github/workflows/docs-preview-pr.yml deleted file mode 100644 index 6c0672ba2..000000000 --- a/.github/workflows/docs-preview-pr.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Docs PR Preview - -on: - pull_request: - branches: [main] - types: [opened, reopened, synchronize, closed] - paths: - - "docs/**" - -concurrency: - group: preview-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: write - pull-requests: write - packages: read - -defaults: - run: - shell: bash - -env: - MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -jobs: - preview: - runs-on: build-arm64 - container: - image: ghcr.io/nvidia/openshell/ci:latest - credentials: - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install tools - run: mise install - - - name: Build documentation - if: github.event.action != 'closed' - run: mise run docs:build:strict - - - name: Delete unnecessary files - if: github.event.action != 'closed' - run: | - find _build -name .doctrees -prune -exec rm -rf {} \; - find _build -name .buildinfo -exec rm {} \; - - - name: Deploy preview - if: github.event.pull_request.head.repo.full_name == github.repository - uses: rossjrw/pr-preview-action@v1 - with: - source-dir: ./_build/docs/ - preview-branch: gh-pages - umbrella-dir: pr-preview - action: auto diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 3535dd204..0d1e3270a 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -399,7 +399,9 @@ jobs: node-version: "24" - name: Install Fern CLI - run: npm install -g fern-api + run: | + FERN_VERSION=$(node -p "require('./fern/fern.config.json').version") + npm install -g "fern-api@${FERN_VERSION}" - name: Publish Fern docs env: diff --git a/AGENTS.md b/AGENTS.md index 405d9aa29..a6cbd29ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,8 +43,8 @@ These pipelines connect skills into end-to-end workflows. Individual skill files | `python/openshell/` | Python SDK | Python bindings and CLI packaging | | `proto/` | Protobuf definitions | gRPC service contracts | | `deploy/` | Docker, Helm, K8s | Dockerfiles, Helm chart, manifests | -| `fern/` | Published docs | Fern site config, navigation, components, and MDX pages | -| `docs/` | Legacy docs source | Sphinx/MyST source retained for migration, comparison, and legacy build tasks | +| `docs/` | Published docs | MDX pages, navigation, and content assets | +| `fern/` | Docs site config | Fern site config, components, and theme assets | | `.agents/skills/` | Agent skills | Workflow automation for development | | `.agents/agents/` | Agent personas | Sub-agent definitions (e.g., reviewer, doc writer) | | `architecture/` | Architecture docs | Design decisions and component documentation | @@ -187,9 +187,9 @@ ocsf_emit!(event); ## Documentation - When making changes, update the relevant documentation in the `architecture/` directory. -- When changes affect user-facing behavior, update the relevant published docs pages under `fern/pages/` and navigation in `fern/versions/latest.yml`. -- `docs/` is retained for legacy Sphinx build tasks and migration/reference work. Do not update it unless the user explicitly asks. -- Follow the docs style guide in [fern/pages/CONTRIBUTING.mdx](fern/pages/CONTRIBUTING.mdx): active voice, minimal formatting, no filler introductions, `shell` fences for copyable commands, and no duplicate body H1. +- When changes affect user-facing behavior, update the relevant published docs pages under `docs/` and navigation in `docs/index.yml`. +- `fern/` contains the Fern site config, components, preview workflow inputs, and publish settings. +- Follow the docs style guide in [docs/CONTRIBUTING.mdx](docs/CONTRIBUTING.mdx): active voice, minimal formatting, no filler introductions, `shell` fences for copyable commands, and no duplicate body H1. - Fern PR previews run through `.github/workflows/branch-docs.yml`, and production publish runs through the `publish-fern-docs` job in `.github/workflows/release-tag.yml`. - Use the `update-docs` skill to scan recent commits and draft doc updates. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7adb3cc4e..55834f603 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -173,7 +173,7 @@ These are the primary `mise` tasks for day-to-day development: | `mise run test` | Default test suite | | `mise run e2e` | Default end-to-end test lane | | `mise run ci` | Full local CI checks (lint, compile/type checks, tests) | -| `mise run docs` | Build legacy Sphinx docs locally | +| `mise run docs` | Validate Fern docs locally | | `mise run clean` | Clean build artifacts | ## Project Structure @@ -185,8 +185,8 @@ These are the primary `mise` tasks for day-to-day development: | `proto/` | Protocol buffer definitions | | `tasks/` | `mise` task definitions and build scripts | | `deploy/` | Dockerfiles, Helm chart, Kubernetes manifests | -| `fern/` | Published Fern docs site and MDX pages | -| `docs/` | Legacy Sphinx/MyST docs retained during transition | +| `docs/` | Published Fern docs source, navigation, and content assets | +| `fern/` | Fern site config, components, and theme assets | | `architecture/` | Architecture docs and plans | | `rfc/` | Request for Comments proposals | | `.agents/` | Agent skills and persona definitions | @@ -197,30 +197,30 @@ For cross-cutting architectural decisions, API contract changes, or process prop ## Documentation -If your change affects user-facing behavior (new flags, changed defaults, new features, bug fixes that contradict existing docs), update the relevant pages under `fern/pages/` in the same PR and adjust `fern/versions/latest.yml` if navigation changes. For explicit navbar entries, keep `page:` aligned with `sidebar-title` when present and put relative `slug:` values in `fern/versions/latest.yml`. Reserve frontmatter `slug` for folder-discovered pages or absolute URL overrides. +If your change affects user-facing behavior (new flags, changed defaults, new features, bug fixes that contradict existing docs), update the relevant pages under `docs/` in the same PR and adjust `docs/index.yml` if navigation changes. For explicit navigation entries, keep `page:` aligned with `sidebar-title` when present and put relative `slug:` values in `docs/index.yml`. Reserve frontmatter `slug` for folder-discovered pages or absolute URL overrides. To ensure your doc changes follow NVIDIA documentation style, use the `update-docs` skill. -It scans commits, identifies doc pages that need updates, and drafts content that follows the style guide in `fern/pages/CONTRIBUTING.mdx`. +It scans commits, identifies doc pages that need updates, and drafts content that follows the style guide in `docs/CONTRIBUTING.mdx`. To preview Fern docs locally: ```bash -fern docs dev +mise run docs:serve ``` To run non-interactive validation: ```bash -fern check +mise run docs ``` -PRs that touch `fern/**` also get a preview from `.github/workflows/branch-docs.yml` when `FERN_TOKEN` is available to the workflow. +PRs that touch `docs/**` or `fern/**` are validated by `.github/workflows/branch-docs.yml`, and they get a preview when `FERN_TOKEN` is available to the workflow. Fern docs publishing is handled by the `publish-fern-docs` job in `.github/workflows/release-tag.yml` when a release tag is created. -`docs/` and `mise run docs` / `mise run docs:serve` are still kept for the legacy Sphinx build during the transition, but they are no longer the primary published docs workflow. +`docs/` is the source-of-truth docs tree. `fern/` contains the site config, components, and theme assets that publish those pages. -See [fern/pages/CONTRIBUTING.mdx](fern/pages/CONTRIBUTING.mdx) for the current docs authoring guide. +See [docs/CONTRIBUTING.mdx](docs/CONTRIBUTING.mdx) for the current docs authoring guide. ## Pull Requests diff --git a/README.md b/README.md index a4bfac641..aaa851452 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ The CLI auto-bootstraps a GPU-enabled gateway on first use, auto-selecting CDI w | `openshell logs [name] --tail` | Stream sandbox logs. | | `openshell term` | Launch the real-time terminal UI for debugging. | -See the full [CLI reference](https://github.com/NVIDIA/OpenShell/blob/main/docs/reference/cli.md) for all commands, flags, and environment variables. +See the [full documentation](https://docs.nvidia.com/openshell/latest) for command guides, tutorials, and reference material. ## Terminal UI @@ -168,7 +168,7 @@ openshell term ```

- OpenShell Terminal UI + OpenShell Terminal UI

The TUI gives you a live, keyboard-driven view of your cluster. Navigate with `Tab` to switch panels, `j`/`k` to move through lists, `Enter` to select, and `:` for command mode. Cluster health and sandbox status auto-refresh every two seconds. @@ -183,7 +183,7 @@ openshell sandbox create --from ./my-sandbox-dir # local Dockerfile openshell sandbox create --from registry.io/img:v1 # container image ``` -See the [community sandboxes](https://github.com/NVIDIA/OpenShell/blob/main/docs/sandboxes/community-sandboxes.md) catalog and the [BYOC example](https://github.com/NVIDIA/OpenShell/tree/main/examples/bring-your-own-container) for details. +See the [community sandboxes](https://docs.nvidia.com/openshell/latest/sandboxes/community-sandboxes) catalog and the [BYOC example](https://github.com/NVIDIA/OpenShell/tree/main/examples/bring-your-own-container) for details. ## Explore with Your Agent @@ -218,10 +218,10 @@ All implementation work is human-gated — agents propose plans, humans approve, ## Learn More - [Full Documentation](https://docs.nvidia.com/openshell/latest/index.html) — overview, architecture, tutorials, and reference -- [Quickstart](https://github.com/NVIDIA/OpenShell/blob/main/docs/get-started/quickstart.md) — detailed install and first sandbox walkthrough -- [GitHub Sandbox Tutorial](https://github.com/NVIDIA/OpenShell/blob/main/docs/tutorials/github-sandbox.md) — end-to-end scoped GitHub repo access +- [Quickstart](https://docs.nvidia.com/openshell/latest/get-started/quickstart) — detailed install and first sandbox walkthrough +- [GitHub Sandbox Tutorial](https://docs.nvidia.com/openshell/latest/tutorials/github-sandbox) — end-to-end scoped GitHub repo access - [Architecture](https://github.com/NVIDIA/OpenShell/tree/main/architecture) — detailed architecture docs and design decisions -- [Support Matrix](https://github.com/NVIDIA/OpenShell/blob/main/docs/reference/support-matrix.md) — platforms, versions, and kernel requirements +- [Support Matrix](https://docs.nvidia.com/openshell/latest/reference/support-matrix) — platforms, versions, and kernel requirements - [Brev Launchable](https://brev.nvidia.com/launchable/deploy/now?launchableID=env-3Ap3tL55zq4a8kew1AuW0FpSLsg) — try OpenShell on cloud compute without local setup - [Agent Instructions](AGENTS.md) — system prompt and workflow documentation for agent contributors diff --git a/architecture/README.md b/architecture/README.md index 1b3b88c2e..570fce660 100644 --- a/architecture/README.md +++ b/architecture/README.md @@ -296,6 +296,7 @@ This opens an interactive SSH session into the sandbox, with all provider creden | [Sandbox Connect](sandbox-connect.md) | SSH tunneling into sandboxes through the gateway. | | [Sandbox Custom Containers](sandbox-custom-containers.md) | Building and using custom container images for sandboxes. | | [Providers](sandbox-providers.md) | External credential management, auto-discovery, and runtime injection. | +| [Docs Site Architecture](docs-site.md) | Documentation source layout, navigation structure, local validation and preview workflow, and publish pipeline. | | [Policy Language](security-policy.md) | The YAML/Rego policy system that governs sandbox behavior. | | [Inference Routing](inference-routing.md) | Transparent interception and sandbox-local routing of AI inference API calls to configured backends. | | [System Architecture](system-architecture.md) | Top-level system architecture diagram with all deployable components and communication flows. | diff --git a/architecture/docs-site.md b/architecture/docs-site.md new file mode 100644 index 000000000..f017ad628 --- /dev/null +++ b/architecture/docs-site.md @@ -0,0 +1,53 @@ +# Docs Site Layout + +## Overview + +Published documentation content lives under `docs/`. The `fern/` directory stores Fern site configuration, React components, theme assets, and publish settings. + +## Repository Layout + +| Path | Role | +|---|---| +| `docs/` | Source of truth for published documentation pages and assets | +| `docs/index.yml` | Navigation definition for the published docs site | +| `fern/docs.yml` | Fern site configuration, including version wiring and publish settings | +| `fern/components/` | Custom Fern React components | +| `fern/assets/` | Site logos and other Fern-managed assets | +| `fern/main.css` | Site theme overrides | +| `fern/fern.config.json` | Fern CLI version and organization config | + +The navigation source is `docs/index.yml`. `fern/docs.yml` points its `versions[].path` field at `../docs/index.yml`, so Fern reads page structure from `docs/` during validation, preview, and publish. + +## Local Workflow + +`tasks/docs.toml` defines the local docs tasks: + +- `mise run docs` runs strict validation. + - Resolves the Fern CLI version from `fern/fern.config.json` + - Runs `fern check` +- `mise run docs:serve` starts a local Fern preview server with `fern docs dev` + +Both tasks execute from `fern/`, but they validate and render the content defined in `docs/`. + +## CI and Release Workflow + +### Pull requests + +`.github/workflows/branch-docs.yml` is the PR docs workflow. + +- It triggers on changes under `docs/**`, `fern/**`, and the workflow file itself. +- It validates the site with `fern check`. +- When `FERN_TOKEN` is available, it runs `fern generate --docs --preview --id pr-` and posts or updates a preview URL on the pull request. + +### Releases + +`.github/workflows/release-tag.yml` publishes production docs in the `publish-fern-docs` job. + +- The job runs after the release job completes. +- It installs the Fern CLI, changes into `./fern`, and runs `fern generate --docs`. + +## Operational Rules + +- Add or edit published pages in `docs/`. +- Change sidebar structure in `docs/index.yml`. +- Change site chrome, theme, Fern behavior, or publish settings in `fern/`. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md deleted file mode 100644 index b77caedc7..000000000 --- a/docs/CONTRIBUTING.md +++ /dev/null @@ -1,185 +0,0 @@ -# Contributing to NVIDIA OpenShell Documentation - -This guide covers how to write, edit, and review documentation for NVIDIA OpenShell. If you change code that affects user-facing behavior, update the relevant docs in the same PR. - -The published docs now live in `fern/versions/latest/pages/`, and navigation is defined in `fern/versions/latest.yml`. The `docs/` tree is kept for legacy Sphinx builds and migration/reference work. Do not update `docs/` unless you are explicitly asked to do so. - -## Use the Agent Skills - -If you use an AI coding agent (Cursor, Claude Code, Codex, etc.), the repo includes skills that automate doc work. Use them before writing from scratch. - -| Skill | What it does | When to use | -|---|---|---| -| `update-docs` | Scans recent commits for user-facing changes and drafts doc updates. | After landing features, before a release, or to find doc gaps. | -| `build-from-issue` | Plans and implements work from a GitHub issue, including doc updates. | When working from an issue that has doc impact. | - -The skills live in `.agents/skills/` and follow the style guide below automatically. To use one, ask your agent to run it (e.g., "catch up the docs for everything merged since v0.2.0"). - -## When to Update Docs - -Update documentation when your change: - -- Adds, removes, or renames a CLI command or flag. -- Changes default behavior or configuration. -- Adds a new feature that users interact with. -- Fixes a bug that the docs describe incorrectly. -- Changes an API, protocol, or policy schema. - -## Building Docs Locally - -Preview Fern docs first, then use the legacy Sphinx build only if you explicitly need it for comparison. - -To preview Fern docs locally, run: - -```shell -fern docs dev -``` - -To run non-interactive validation, run: - -```shell -fern check -``` - -PRs that touch `fern/**` also get a preview from `.github/workflows/branch-docs.yml` when `FERN_TOKEN` is available to the workflow. - -If you need the legacy Sphinx build during the transition, use: - -```bash -mise run docs -``` - -To serve the legacy docs locally with automatic rebuilds, run: - -```bash -mise run docs:serve -``` - -## Writing Conventions - -### Format - -- Published docs use Fern MDX under `fern/versions/latest/pages/`. -- Every page starts with YAML frontmatter. Use `title` and `description` on every page, then add page-level metadata like `sidebar-title`, `keywords`, `tags`, and `position` when the page needs them. -- Include the SPDX license header after frontmatter: - ``` - {/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} - ``` -- Do not repeat the page title as a body H1. Fern renders the title from frontmatter. - -### Frontmatter Template - -```yaml ---- -title: "Page Title" -sidebar-title: "Short Nav Title" -description: "One-sentence summary of the page." -keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing" -tags: - - AI Agents - - Sandboxing ---- -``` - -- `title` sets the page heading and browser title. -- `sidebar-title` sets the shorter label in the sidebar when the full page title is too long. -- `keywords` is a comma-separated string for page metadata. When migrating from `docs/`, combine the legacy `topics` and `tags` into `keywords`. -- `tags` is an optional array for preserving legacy doc taxonomy or changelog tags. Fern does not treat `tags` as SEO metadata on ordinary pages, so still populate `keywords`. -- `position` controls ordering for pages discovered through a `folder:` entry. -- `slug` optionally overrides the page URL with a full path from the docs root. - -For explicit entries in `fern/versions/latest.yml`, keep `page:`. Fern still requires it. If the page defines `sidebar-title`, set `page:` to that value. Otherwise set `page:` to the frontmatter `title`. - -For explicit page and folder entries in `fern/versions/latest.yml`, put the relative `slug:` there instead of in frontmatter. Use `skip-slug: true` when a child page should live at the parent section path. - -### Page Structure - -1. Frontmatter `title` and `description`, plus any relevant page metadata. -2. A one- or two-sentence introduction stating what the page covers. -3. Sections organized by task or concept, using H2 and H3. Start each section with an introductory sentence that orients the reader. -4. A "Next Steps" section at the bottom linking to related pages when it helps the reader continue. - -## Style Guide - -Write like you are explaining something to a colleague. Be direct, specific, and concise. - -### Voice and Tone - -- Use active voice. "The CLI creates a gateway" not "A gateway is created by the CLI." -- Use second person ("you") when addressing the reader. -- Use present tense. "The command returns an error" not "The command will return an error." -- State facts. Do not hedge with "simply," "just," "easily," or "of course." - -### Things to Avoid - -These patterns are common in LLM-generated text and erode trust with technical readers. Remove them during review. - -| Pattern | Problem | Fix | -|---|---|---| -| Unnecessary bold | "This is a **critical** step" on routine instructions. | Reserve bold for UI labels, parameter names, and genuine warnings. | -| Em dashes everywhere | "The gateway — which runs in Docker — creates sandboxes." | Use commas or split into two sentences. Em dashes are fine sparingly but should not appear multiple times per paragraph. | -| Superlatives | "OpenShell provides a powerful, robust, seamless experience." | Say what it does, not how great it is. | -| Hedge words | "Simply run the command" or "You can easily configure..." | Drop the adverb. "Run the command." | -| Emoji in prose | "🚀 Let's get started!" | No emoji in documentation prose. | -| Rhetorical questions | "Want to secure your agents? Look no further!" | State the purpose directly. | - -### Formatting Rules - -- End every sentence with a period. -- Use `code` formatting for CLI commands, file paths, flags, parameter names, and values. -- Use `shell` code blocks for copyable CLI examples. Do not prefix commands with `$`: - ```shell - openshell gateway start - ``` -- Use `text` code blocks for transcripts, log output, and examples that should not be copied verbatim. -- Use tables for structured comparisons. Keep tables simple (no nested formatting). -- Use Fern components like ``, ``, and `` for callouts, not bold text. -- Use Fern components like `` and `` when the page clearly benefits from them. -- Do not number section titles. Write "Deploy a Gateway" not "Section 1: Deploy a Gateway" or "Step 3: Verify." -- Do not use colons in titles. Write "Deploy and Manage Gateways" not "Gateways: Deploy and Manage." -- Use colons only to introduce a list. Do not use colons as general-purpose punctuation between clauses. - -### Word List - -Use these consistently: - -| Use | Do not use | -|---|---| -| gateway | Gateway (unless starting a sentence) | -| sandbox | Sandbox (unless starting a sentence) | -| CLI | cli, Cli | -| API key | api key, API Key | -| NVIDIA | Nvidia, nvidia | -| OpenShell | Open Shell, openShell, Openshell, openshell | -| mTLS | MTLS, mtls | -| YAML | yaml, Yaml | - -## Submitting Doc Changes - -1. Create a branch following the project convention: `docs/-/`. -2. Make your changes. -3. Preview locally with `fern docs dev`. -4. Run `fern check`. -5. Run `mise run pre-commit` to catch formatting issues. -6. Open a PR with `docs:` as the conventional commit type. - -``` -docs: update gateway deployment instructions -``` - -If your doc change accompanies a code change, include both in the same PR and use the code change's commit type: - -``` -feat(cli): add --gpu flag to gateway start -``` - -## Reviewing Doc PRs - -When reviewing documentation: - -- Check that the style guide rules above are followed. -- Watch for LLM-generated patterns (excessive bold, em dashes, filler). -- Verify code examples are accurate and runnable. -- Confirm cross-references and links are not broken. -- Preview the page with `fern docs dev`, run `fern check`, and, if available, review the PR preview from `branch-docs.yml`. diff --git a/fern/pages/CONTRIBUTING.mdx b/docs/CONTRIBUTING.mdx similarity index 79% rename from fern/pages/CONTRIBUTING.mdx rename to docs/CONTRIBUTING.mdx index b3eb06cbc..1fcaf322a 100644 --- a/fern/pages/CONTRIBUTING.mdx +++ b/docs/CONTRIBUTING.mdx @@ -1,10 +1,12 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Contributing to NVIDIA OpenShell Documentation" description: "" --- This guide covers how to write, edit, and review documentation for NVIDIA OpenShell. If you change code that affects user-facing behavior, update the relevant docs in the same PR. -The published docs live in `fern/pages/`, and navigation is defined in `fern/versions/latest.yml`. The `docs/` tree is kept for legacy Sphinx builds and migration/reference work. Do not update `docs/` unless you are explicitly asked to do so. +The published docs live in `docs/`, navigation is defined in `docs/index.yml`, and `fern/` contains the site config, components, and theme assets. ## Use the Agent Skills @@ -29,44 +31,37 @@ Update documentation when your change: ## Building Docs Locally -Preview Fern docs first, then use the legacy Sphinx build only if you explicitly need it for comparison. +Use the local `mise` tasks for preview and validation, or run the Fern CLI directly from `fern/` if you already have it installed. To preview Fern docs locally, run: ```shell -fern docs dev +mise run docs:serve ``` To run non-interactive validation, run: -```shell -fern check -``` - -PRs that touch `fern/**` also get a preview from `.github/workflows/branch-docs.yml` when `FERN_TOKEN` is available to the workflow. - -If you need the legacy Sphinx build during the transition, use: - ```shell mise run docs ``` -To serve the legacy docs locally and automatically rebuild on changes, run: +If you already have the Fern CLI installed, the equivalent commands from `fern/` are `fern docs dev` and `fern check`. -```shell -mise run docs:serve -``` +PRs that touch `docs/**` or `fern/**` are validated by `.github/workflows/branch-docs.yml`, and they also get a preview when `FERN_TOKEN` is available to the workflow. ## Writing Conventions ### Format -- Published docs use Fern MDX under `fern/pages/`. -- Every page starts with YAML frontmatter. Use `title` and `description` on every page, then add page-level metadata like `sidebar-title`, `keywords`, `tags`, and `position` when the page needs them. -- Include the SPDX license header after frontmatter: +- Published docs use Fern MDX under `docs/`. +- Every page starts with YAML frontmatter. Use `title` and `description` on every page, then add page-level metadata like `sidebar-title`, `keywords`, and `position` when the page needs them. +- Include the SPDX license header as YAML comments inside frontmatter: ``` - {/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} + --- + # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + title: "Page Title" + --- ``` - Do not repeat the page title as a body H1. Fern renders the title from frontmatter. @@ -74,24 +69,22 @@ mise run docs:serve ```yaml --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Page Title" sidebar-title: "Short Nav Title" description: "One-sentence summary of the page." keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing" -tags: - - AI Agents - - Sandboxing --- ``` - `title` sets the page heading and browser title. - `sidebar-title` sets the shorter label in the sidebar when the full page title is too long. -- `keywords` is a comma-separated string for page metadata. When migrating from `docs/`, combine the legacy `topics` and `tags` into `keywords`. -- `tags` is an optional array for preserving legacy doc taxonomy or changelog tags. Fern does not treat `tags` as SEO metadata on ordinary pages, so still populate `keywords`. +- `keywords` is a comma-separated string for page metadata. - `position` controls ordering for pages discovered through a `folder:` entry. - `slug` optionally overrides the page URL with a full path from the docs root. -For explicit entries in `fern/versions/latest.yml`, keep `page:`. Fern still requires it. If the page defines `sidebar-title`, set `page:` to that value. Otherwise set `page:` to the frontmatter `title`. +For explicit entries in `docs/index.yml`, keep `page:`. Fern still requires it. If the page defines `sidebar-title`, set `page:` to that value. Otherwise set `page:` to the frontmatter `title`. ### Page Structure @@ -159,8 +152,8 @@ Use these consistently: 1. Create a branch following the project convention: `docs/-/`. 2. Make your changes. -3. Preview locally with `fern docs dev`. -4. Run `fern check`. +3. Preview locally with `mise run docs:serve`. +4. Run `mise run docs`. 5. Run `mise run pre-commit` to catch formatting issues. 6. Open a PR with `docs:` as the conventional commit type. diff --git a/fern/components/BadgeLinks.tsx b/docs/_components/BadgeLinks.tsx similarity index 100% rename from fern/components/BadgeLinks.tsx rename to docs/_components/BadgeLinks.tsx diff --git a/docs/_ext/json_output/README.md b/docs/_ext/json_output/README.md deleted file mode 100644 index a0d966f43..000000000 --- a/docs/_ext/json_output/README.md +++ /dev/null @@ -1,295 +0,0 @@ -# JSON Output Extension - -Sphinx extension to generate JSON output for every page alongside HTML output. - -Similar to Hugo's output formats, this creates parallel JSON files for each document -containing metadata, content, and other structured data that can be consumed by -search engines, APIs, or other applications. - -The main use case is generating comprehensive search indexes for tools like Solr, -Lunr.js, or custom search implementations. - -## Search Index Integration - -The main index.json file contains all documents with full content, perfect for: - -- **Lunr.js**: Load index.json and build search index from documents -- **Solr**: POST the JSON data to Solr's update endpoint -- **Elasticsearch**: Bulk index the documents array -- **Custom search**: Parse JSON and implement your own search logic - -## Enhanced JSON Structure - -The JSON structure includes search-optimized fields and global metadata from `conf.py`: - -```json -{ - "id": "getting-started/installation-guide", - "title": "Installation Guide", - "url": "/getting-started/installation-guide.html", - "last_modified": "2026-01-15T10:30:00Z", - - "book": { - "title": "NVIDIA NeMo Guardrails Library Developer Guide", - "version": "0.11.0" - }, - "product": { - "name": "NeMo Guardrails", - "family": ["NeMo"], - "version": "0.11.0" - }, - "site": { - "name": "NVIDIA Technical Documentation" - }, - - "content": "Full markdown content here...", - "content_length": 5420, - "word_count": 850, - "format": "text", - "summary": "Quick summary for previews...", - "doc_type": "tutorial", - "section_path": ["Getting Started", "Installation Guide"], - "headings": [ - {"text": "Prerequisites", "level": 2, "id": "prerequisites"} - ], - "headings_text": "Prerequisites Installation Steps Troubleshooting", - "keywords": ["install", "setup", "prerequisites", "pip", "python", "guardrails"], - "code_blocks": [ - {"content": "pip install nemoguardrails", "language": "bash"} - ], - "links": [ - { - "text": "Configuration Guide", - "url": "/configure-rails/index.html", - "type": "cross_reference", - "ref_type": "doc", - "target_doc": "configure-rails/index" - }, - { - "text": "GitHub Repository", - "url": "https://github.com/NVIDIA/NeMo-Guardrails", - "type": "external" - } - ], - "tags": ["setup", "guide"], - "categories": ["tutorials"] -} -``` - -## Configuration Examples - -### Minimal Configuration (Recommended) - -Uses optimized defaults for best performance: - -```python -# conf.py -json_output_settings = { - 'enabled': True, # All other settings use performance-optimized defaults -} -``` - -### Comprehensive Search Index (Default Behavior) - -```python -json_output_settings = { - 'enabled': True, - 'verbose': True, # Default: detailed logging - 'parallel': True, # Default: parallel processing - 'main_index_mode': 'full', # Default: full content - 'max_main_index_docs': 0, # Default: no limit - 'minify_json': True, # Default: smaller files - 'filter_search_clutter': True, # Default: clean content -} -``` - -### Large Sites Configuration - -```python -json_output_settings = { - 'enabled': True, - 'max_main_index_docs': 500, # Limit to 500 documents - 'content_max_length': 20000, # Limit content length - 'skip_large_files': 50000, # Skip files over 50KB -} -``` - -### Fastest Builds (Minimal Features) - -```python -json_output_settings = { - 'enabled': True, - 'main_index_mode': 'metadata_only', # Only titles, descriptions, tags - 'lazy_extraction': True, # Skip keywords, links, code_blocks, images - 'skip_complex_parsing': True, # Skip complex parsing features -} -``` - -## Available Settings - -### Core Settings - -- **enabled** (bool): Enable/disable JSON output generation. Default: `True` -- **verbose** (bool): Enable verbose logging. Default: `True` -- **parallel** (bool): Enable parallel processing. Default: `True` -- **exclude_patterns** (list): Patterns to exclude from JSON generation. Default: `['_build', '_templates', '_static']` -- **include_children** (bool): Include child documents in directory indexes. Default: `True` -- **include_child_content** (bool): Include full content in child documents. Default: `True` -- **main_index_mode** (str): How to handle main index page. Options: `'disabled'`, `'metadata_only'`, `'full'`. Default: `'full'` -- **max_main_index_docs** (int): Maximum documents to include in main index (0 = no limit). Default: `0` - -### Search Optimization Features - -- **extract_code_blocks** (bool): Include code blocks in search data. Default: `True` -- **extract_links** (bool): Include internal/external links. Default: `True` -- **extract_images** (bool): Include image references. Default: `True` -- **extract_keywords** (bool): Auto-extract technical keywords (frontmatter `keywords` field takes priority). Default: `True` -- **include_doc_type** (bool): Auto-detect document types (tutorial, guide, reference, etc.). Default: `True` -- **include_section_path** (bool): Include hierarchical section paths. Default: `True` - -### Link Extraction Options - -- **link_normalization** (bool): Normalize internal URLs to absolute paths with `.html` extension. Default: `True` -- **link_include_ref_type** (bool): Include `ref_type` metadata (ref, doc, any, etc.) for cross-references. Default: `True` -- **link_include_target_doc** (bool): Include `target_doc` for cross-references (enables document relationship mapping). Default: `True` -- **link_resolve_titles** (bool): Resolve filename-like link text (e.g., "index") to document titles (e.g., "Getting Started"). Default: `True` - -### Performance Controls - -- **content_max_length** (int): Max content length per document (0 = no limit). Default: `50000` -- **summary_max_length** (int): Max summary length. Default: `500` -- **keywords_max_count** (int): Max keywords per document. Default: `50` - -### Output Format Options - -- **minify_json** (bool): Minify JSON output (removes indentation for smaller files). Default: `True` -- **separate_content** (bool): Store content in separate .content.json files for better performance. Default: `False` - -### Speed Optimizations - -- **parallel_workers** (str): Number of parallel workers. Default: `'auto'` -- **batch_size** (int): Process documents in batches. Default: `50` -- **cache_aggressive** (bool): Enable aggressive caching. Default: `True` -- **lazy_extraction** (bool): Skip feature extraction (keywords, links, code_blocks, images) for faster builds. Default: `False` -- **skip_large_files** (int): Skip files larger than N bytes. Default: `100000` -- **incremental_build** (bool): Only process changed files. Default: `True` -- **memory_limit_mb** (int): Memory limit per worker. Default: `512` -- **fast_text_extraction** (bool): Use faster text extraction. Default: `True` -- **skip_complex_parsing** (bool): Skip complex parsing features. Default: `False` - -### Content Filtering - -- **filter_search_clutter** (bool): Remove SVG, toctree, and other non-searchable content. Default: `True` - -### Global Metadata - -- **global_metadata** (dict): User-defined global fields injected into all JSON files. Default: `{}` -- **infer_global_metadata** (bool): Auto-infer book/product/site from Sphinx config. Default: `True` - -## Global Metadata from conf.py - -The extension can inject site-wide metadata from `conf.py` into every JSON file, providing consistent book/product/site context without requiring frontmatter on each page. - -### Auto-Inference (Default) - -By default, the extension auto-infers global metadata from standard Sphinx configuration: - -| JSON Field | Source | Example | -|------------|--------|---------| -| `book.title` | `project` | "NVIDIA NeMo Guardrails Library Developer Guide" | -| `book.version` | `release` | "0.11.0" | -| `product.name` | Extracted from `project` (strips "NVIDIA" prefix and doc suffixes) | "NeMo Guardrails" | -| `product.version` | `release` | "0.11.0" | -| `product.family` | `html_context["product_family"]` (if set) | ["NeMo"] | -| `site.name` | `html_context["site_name"]` (if set) | "NVIDIA Technical Documentation" | - -### Explicit Configuration - -For full control, provide explicit `global_metadata`: - -```python -# conf.py -project = "NVIDIA NeMo Guardrails Library Developer Guide" -release = "0.11.0" - -json_output_settings = { - "enabled": True, - "global_metadata": { - "book": { - "title": project, - "version": release, - }, - "product": { - "name": "NeMo Guardrails", - "family": ["NeMo"], - "version": release, - }, - "site": { - "name": "NVIDIA Technical Documentation", - }, - }, -} -``` - -### Using html_context for Inference - -You can also set values via `html_context` for auto-inference: - -```python -# conf.py -project = "NVIDIA NeMo Guardrails Library Developer Guide" -release = "0.11.0" - -html_context = { - "product_name": "NeMo Guardrails", - "product_family": ["NeMo"], - "site_name": "NVIDIA Technical Documentation", -} - -json_output_settings = { - "enabled": True, - "infer_global_metadata": True, # Default -} -``` - -### Disabling Global Metadata - -To disable global metadata entirely: - -```python -json_output_settings = { - "enabled": True, - "infer_global_metadata": False, - "global_metadata": {}, -} -``` - -## Content Gating Integration - -This extension automatically respects content gating rules set by the content_gating extension at multiple levels: - -### Document-Level Gating - -Documents with 'only' conditions in frontmatter that fail evaluation (e.g., 'only: not ga' when building with -t ga) will be excluded from JSON generation entirely, ensuring sensitive content doesn't leak into search indexes. - -### Content-Level Gating - -Content sections wrapped in `{conditional}` directives are also properly filtered. When conditions don't match, the content is excluded from the document tree and won't appear in the generated JSON. - -### Integration Details - -- **Automatic Detection**: Detects if content_gating extension is loaded -- **Exclude Pattern Sync**: Respects documents added to exclude_patterns by content gating -- **Build Tag Awareness**: Logs current build tags for debugging -- **Debug Logging**: Provides detailed logs when content gating rules are applied - -The integration works seamlessly - just enable both extensions and your JSON output will automatically respect all content gating rules without additional configuration. - -## Performance Tips - -1. **Enable parallel processing** for faster builds on multi-core systems -2. **Use incremental builds** to only process changed files -3. **Set content length limits** for large documentation sites -4. **Enable content filtering** to reduce JSON file sizes -5. **Use batch processing** to control memory usage -6. **Skip large files** to avoid processing massive documents diff --git a/docs/_ext/json_output/__init__.py b/docs/_ext/json_output/__init__.py deleted file mode 100644 index 447af75b1..000000000 --- a/docs/_ext/json_output/__init__.py +++ /dev/null @@ -1,48 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Sphinx extension to generate JSON output for every page alongside HTML output. - -This extension creates parallel JSON files for each document containing metadata, -content, and other structured data that can be consumed by search engines, APIs, -or other applications. - -See README.md for detailed configuration options and usage examples. -""" - -from typing import Any - -from sphinx.application import Sphinx - -from .config import get_default_settings, validate_config -from .processing import on_build_finished - - -def setup(app: Sphinx) -> dict[str, Any]: - """Setup function for Sphinx extension.""" - # Add configuration with default settings - default_settings = get_default_settings() - app.add_config_value("json_output_settings", default_settings, "html") - - # Connect to build events - app.connect("config-inited", validate_config) - app.connect("build-finished", on_build_finished) - - return { - "version": "1.0.0", - "parallel_read_safe": True, - "parallel_write_safe": True, - } diff --git a/docs/_ext/json_output/config.py b/docs/_ext/json_output/config.py deleted file mode 100644 index de9e3315a..000000000 --- a/docs/_ext/json_output/config.py +++ /dev/null @@ -1,226 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Configuration management for JSON output extension.""" - -from typing import Any - -from sphinx.application import Sphinx -from sphinx.config import Config -from sphinx.util import logging - -logger = logging.getLogger(__name__) - -# Constants -MAX_PARALLEL_WORKERS = 32 - - -def get_default_settings() -> dict[str, Any]: - """Get default configuration settings for json_output extension.""" - return { - "enabled": True, - "exclude_patterns": ["_build", "_templates", "_static"], - "verbose": True, # Enable by default for better user feedback - "parallel": True, # Enable parallel processing by default for speed - "include_children": True, - "include_child_content": True, - "main_index_mode": "full", # 'disabled', 'metadata_only', 'full' - "max_main_index_docs": 0, # No limit by default for comprehensive search - # Search optimization features - "extract_code_blocks": True, # Include code blocks in search data - "extract_links": True, # Include internal/external links - "extract_images": True, # Include image references - "extract_keywords": True, # Auto-extract technical keywords - "include_doc_type": True, # Auto-detect document types - "include_section_path": True, # Include hierarchical section paths - # Link extraction options - "link_normalization": True, # Normalize internal URLs to absolute paths - "link_include_ref_type": True, # Include ref_type metadata (ref, doc, etc.) - "link_include_target_doc": True, # Include target_doc for cross-references - "link_resolve_titles": True, # Resolve filename-like link text to document titles - # Performance controls - "content_max_length": 50000, # Max content length per document (0 = no limit) - "summary_max_length": 500, # Max summary length - "keywords_max_count": 50, # Max keywords per document - # Output format options - "minify_json": True, # Minify JSON by default for better performance - "separate_content": False, # Store content in separate .content.json files - # Speed optimizations - "parallel_workers": "auto", # Number of parallel workers - "batch_size": 50, # Process documents in batches - "cache_aggressive": True, # Enable aggressive caching - "lazy_extraction": False, # Skip feature extraction (keywords, links, etc.) for faster builds - "skip_large_files": 100000, # Skip files larger than N bytes - "incremental_build": True, # Only process changed files - "memory_limit_mb": 512, # Memory limit per worker - "fast_text_extraction": True, # Use faster text extraction - "skip_complex_parsing": False, # Skip complex parsing features - # Content filtering - "filter_search_clutter": True, # Remove SVG, toctree, and other non-searchable content - # Global metadata from conf.py - "global_metadata": {}, # User-defined global fields (book, product, site) - "infer_global_metadata": True, # Auto-infer from Sphinx config (project, release) - } - - -def apply_config_defaults(settings: dict[str, Any]) -> dict[str, Any]: - """Apply default values to settings dictionary.""" - defaults = get_default_settings() - - for key, default_value in defaults.items(): - if key not in settings: - settings[key] = default_value - - return settings - - -def validate_config(_app: Sphinx, config: Config) -> None: - """Validate configuration values.""" - settings = _ensure_settings_dict(config) - settings = apply_config_defaults(settings) - config.json_output_settings = settings - - _validate_core_settings(settings) - _validate_content_limits(settings) - _validate_boolean_settings(settings) - _validate_integer_settings(settings) - _validate_parallel_workers(settings) - _validate_global_metadata(settings) - - -def _ensure_settings_dict(config: Config) -> dict[str, Any]: - """Ensure settings is a valid dictionary.""" - settings = getattr(config, "json_output_settings", {}) - if not isinstance(settings, dict): - logger.warning("json_output_settings must be a dictionary. Using defaults.") - settings = {} - config.json_output_settings = settings - return settings - - -def _validate_core_settings(settings: dict[str, Any]) -> None: - """Validate core configuration settings.""" - # Validate main index mode - valid_modes = ["disabled", "metadata_only", "full"] - mode = settings.get("main_index_mode", "full") - if mode not in valid_modes: - logger.warning(f"Invalid main_index_mode '{mode}'. Using 'full'. Valid options: {valid_modes}") - settings["main_index_mode"] = "full" - - # Validate exclude patterns - patterns = settings.get("exclude_patterns", []) - if not isinstance(patterns, list): - logger.warning("exclude_patterns must be a list. Using default.") - settings["exclude_patterns"] = ["_build", "_templates", "_static"] - - -def _validate_content_limits(settings: dict[str, Any]) -> None: - """Validate content-related limit settings.""" - limit_settings = { - "max_main_index_docs": (0, "0 (no limit)"), - "content_max_length": (50000, "50000 (0 = no limit)"), - "summary_max_length": (500, "500"), - "keywords_max_count": (50, "50"), - } - - for setting, (default_val, description) in limit_settings.items(): - value = settings.get(setting, default_val) - if not isinstance(value, int) or value < 0: - logger.warning(f"Invalid {setting} '{value}'. Using {description}.") - settings[setting] = default_val - - -def _validate_boolean_settings(settings: dict[str, Any]) -> None: - """Validate boolean configuration settings.""" - bool_settings = [ - "enabled", - "verbose", - "parallel", - "include_children", - "include_child_content", - "extract_code_blocks", - "extract_links", - "extract_images", - "extract_keywords", - "include_doc_type", - "include_section_path", - "link_normalization", - "link_include_ref_type", - "link_include_target_doc", - "link_resolve_titles", - "minify_json", - "separate_content", - "cache_aggressive", - "lazy_extraction", - "incremental_build", - "fast_text_extraction", - "skip_complex_parsing", - "filter_search_clutter", - "infer_global_metadata", - ] - - defaults = get_default_settings() - for setting in bool_settings: - if setting in settings and not isinstance(settings.get(setting), bool): - logger.warning(f"Setting '{setting}' must be boolean. Using default.") - settings[setting] = defaults[setting] - - -def _validate_integer_settings(settings: dict[str, Any]) -> None: - """Validate integer configuration settings with ranges.""" - int_settings = { - "batch_size": (1, 1000), # min, max - "skip_large_files": (0, None), # 0 = disabled - "memory_limit_mb": (64, 8192), # reasonable memory limits - } - - defaults = get_default_settings() - for setting, (min_val, max_val) in int_settings.items(): - if setting in settings: - value = settings[setting] - if not isinstance(value, int) or value < min_val or (max_val and value > max_val): - logger.warning( - f"Setting '{setting}' must be integer between {min_val} and {max_val or 'unlimited'}. Using default." - ) - settings[setting] = defaults[setting] - - -def _validate_parallel_workers(settings: dict[str, Any]) -> None: - """Validate parallel_workers setting (can be 'auto' or integer).""" - if "parallel_workers" in settings: - value = settings["parallel_workers"] - if value != "auto" and (not isinstance(value, int) or value < 1 or value > MAX_PARALLEL_WORKERS): - logger.warning( - f"Setting 'parallel_workers' must be 'auto' or integer between 1 and {MAX_PARALLEL_WORKERS}. Using default." - ) - defaults = get_default_settings() - settings["parallel_workers"] = defaults["parallel_workers"] - - -def _validate_global_metadata(settings: dict[str, Any]) -> None: - """Validate global_metadata setting structure.""" - global_metadata = settings.get("global_metadata", {}) - - if not isinstance(global_metadata, dict): - logger.warning("global_metadata must be a dictionary. Using empty default.") - settings["global_metadata"] = {} - return - - # Validate known top-level keys have dict values - valid_sections = ["book", "product", "site"] - for section in valid_sections: - if section in global_metadata and not isinstance(global_metadata[section], dict): - logger.warning(f"global_metadata.{section} must be a dictionary. Removing invalid value.") - del global_metadata[section] diff --git a/docs/_ext/json_output/content/__init__.py b/docs/_ext/json_output/content/__init__.py deleted file mode 100644 index abc7b45ee..000000000 --- a/docs/_ext/json_output/content/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Content extraction functions for JSON output.""" - -from .extractor import extract_document_content -from .metadata import extract_document_metadata - -__all__ = [ - "extract_document_content", - "extract_document_metadata", -] diff --git a/docs/_ext/json_output/content/extractor.py b/docs/_ext/json_output/content/extractor.py deleted file mode 100644 index 9cf975650..000000000 --- a/docs/_ext/json_output/content/extractor.py +++ /dev/null @@ -1,245 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Main content extraction orchestration.""" - -from typing import Any - -from docutils import nodes -from sphinx.environment import BuildEnvironment -from sphinx.util import logging - -from .structured import extract_code_blocks, extract_headings, extract_images, extract_links -from .text import ( - clean_text_for_llm, - extract_clean_text_content, - extract_keywords, - extract_raw_markdown, - extract_summary, - extract_text_content, -) - -logger = logging.getLogger(__name__) - - -def extract_document_content(env: BuildEnvironment, docname: str, content_cache: dict) -> dict[str, Any]: - """Extract content from document optimized for LLM/search use cases.""" - if docname in content_cache: - return content_cache[docname] - - try: - logger.debug(f"Starting content extraction for {docname}") - doctree = env.get_doctree(docname) - - # Get extraction settings - extraction_settings = _get_extraction_settings(env) - - # Extract main content - content = _extract_main_content(doctree, env, docname, extraction_settings) - - # Extract additional features based on settings (pass env for link resolution) - _extract_additional_features(content, doctree, docname, extraction_settings, env) - - # Cache and return result - content_cache[docname] = content - logger.debug(f"Successfully extracted content for {docname}") - - except Exception: - logger.exception(f"Critical error extracting content from {docname}") - content = _get_empty_content_dict() - content_cache[docname] = content - - return content_cache[docname] - - -def _get_extraction_settings(env: BuildEnvironment) -> dict[str, bool]: - """Extract all extraction-related settings from environment config.""" - config = getattr(env.app, "config", None) - json_settings = getattr(config, "json_output_settings", {}) if config else {} - - return { - "fast_extraction": json_settings.get("fast_text_extraction", False), - "lazy_extraction": json_settings.get("lazy_extraction", False), - "skip_complex": json_settings.get("skip_complex_parsing", False), - "filter_clutter": json_settings.get("filter_search_clutter", True), - } - - -def _extract_main_content( - doctree: nodes.document, env: BuildEnvironment, docname: str, settings: dict[str, bool] -) -> dict[str, Any]: - """Extract main text content with appropriate strategy.""" - content = {} - - try: - if settings["fast_extraction"]: - content["content"] = extract_text_content(doctree) - content["format"] = "text" - logger.debug(f"Fast text extraction for {docname}: {len(content['content'])} chars") - else: - content = _extract_with_fallbacks(doctree, env, docname) - - # Apply content filtering if enabled - if settings["filter_clutter"] and content.get("content"): - _apply_content_filtering(content, docname) - - except Exception as e: # noqa: BLE001 - logger.warning(f"Error extracting main content from {docname}: {e}") - content = {"content": "", "format": "text"} - - return content - - -def _extract_with_fallbacks(doctree: nodes.document, env: BuildEnvironment, docname: str) -> dict[str, Any]: - """Extract content with multiple fallback strategies.""" - # Try clean text first (pass env for link title resolution) - clean_text = extract_clean_text_content(doctree, env) - if clean_text: - logger.debug(f"Extracted clean text content for {docname}: {len(clean_text)} chars") - return {"content": clean_text, "format": "text"} - - # Fallback to raw markdown - raw_markdown = extract_raw_markdown(env, docname) - if raw_markdown: - logger.debug(f"Fallback to raw markdown for {docname}: {len(raw_markdown)} chars") - return {"content": raw_markdown, "format": "markdown"} - - # Final fallback to basic text - logger.debug(f"Fallback to basic text extraction for {docname}") - return {"content": extract_text_content(doctree), "format": "text"} - - -def _apply_content_filtering(content: dict[str, Any], docname: str) -> None: - """Apply content filtering to remove clutter.""" - original_length = len(content["content"]) - content["content"] = clean_text_for_llm(content["content"]) - filtered_length = len(content["content"]) - - if original_length != filtered_length: - logger.debug(f"Content filtering for {docname}: {original_length} -> {filtered_length} chars") - - -def _extract_additional_features( - content: dict[str, Any], - doctree: nodes.document, - docname: str, - settings: dict[str, bool], - env: BuildEnvironment | None = None, -) -> None: - """Extract additional features based on extraction settings.""" - if settings["lazy_extraction"]: - _set_empty_additional_features(content) - return - - # Extract basic features - _extract_basic_features(content, doctree, docname) - - # Extract complex features if not skipped - if not settings["skip_complex"]: - _extract_complex_features(content, doctree, docname, env) - else: - _set_empty_complex_features(content) - - # Extract keywords if not lazy - if not settings["lazy_extraction"]: - _extract_keywords_feature(content, docname) - else: - content["keywords"] = [] - - -def _extract_basic_features(content: dict[str, Any], doctree: nodes.document, docname: str) -> None: - """Extract basic features: headings and summary.""" - features = [ - ("headings", extract_headings, []), - ("summary", extract_summary, ""), - ] - - for feature_name, extract_func, default_value in features: - try: - result = extract_func(doctree) - content[feature_name] = result - if feature_name == "headings": - logger.debug(f"Extracted {len(result)} headings from {docname}") - except Exception as e: # noqa: BLE001, PERF203 - logger.warning(f"Error extracting {feature_name} from {docname}: {e}") - content[feature_name] = default_value - - -def _extract_complex_features( - content: dict[str, Any], - doctree: nodes.document, - docname: str, - env: BuildEnvironment | None = None, -) -> None: - """Extract complex features: code blocks, links, and images.""" - # Code blocks and images don't need env - simple_features = [ - ("code_blocks", extract_code_blocks), - ("images", extract_images), - ] - - for feature_name, extract_func in simple_features: - try: - result = extract_func(doctree) - content[feature_name] = result - logger.debug(f"Extracted {len(result)} {feature_name} from {docname}") - except Exception as e: # noqa: BLE001, PERF203 - logger.warning(f"Error extracting {feature_name} from {docname}: {e}") - content[feature_name] = [] - - # Links need env for title resolution - try: - content["links"] = extract_links(doctree, env, docname) - logger.debug(f"Extracted {len(content['links'])} links from {docname}") - except Exception as e: # noqa: BLE001 - logger.warning(f"Error extracting links from {docname}: {e}") - content["links"] = [] - - -def _extract_keywords_feature(content: dict[str, Any], docname: str) -> None: - """Extract keywords from content and headings.""" - try: - content["keywords"] = extract_keywords(content.get("content", ""), content.get("headings", [])) - logger.debug(f"Extracted {len(content['keywords'])} keywords from {docname}") - except Exception as e: # noqa: BLE001 - logger.warning(f"Error extracting keywords from {docname}: {e}") - content["keywords"] = [] - - -def _set_empty_additional_features(content: dict[str, Any]) -> None: - """Set empty values for all additional features (lazy extraction).""" - features = ["headings", "summary", "code_blocks", "links", "images", "keywords"] - for feature in features: - content[feature] = [] if feature != "summary" else "" - - -def _set_empty_complex_features(content: dict[str, Any]) -> None: - """Set empty values for complex features only.""" - for feature in ["code_blocks", "links", "images"]: - content[feature] = [] - - -def _get_empty_content_dict() -> dict[str, Any]: - """Get empty content dictionary for error cases.""" - return { - "content": "", - "format": "text", - "headings": [], - "summary": "", - "code_blocks": [], - "links": [], - "images": [], - "keywords": [], - } diff --git a/docs/_ext/json_output/content/metadata.py b/docs/_ext/json_output/content/metadata.py deleted file mode 100644 index 03c543d96..000000000 --- a/docs/_ext/json_output/content/metadata.py +++ /dev/null @@ -1,94 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Metadata and frontmatter extraction functions.""" - -from typing import Any - -from sphinx.environment import BuildEnvironment -from sphinx.util import logging - -# Import YAML at module level with error handling -try: - import yaml - - YAML_AVAILABLE = True -except ImportError: - YAML_AVAILABLE = False - yaml = None - -logger = logging.getLogger(__name__) - - -def extract_document_metadata( - env: BuildEnvironment, docname: str, metadata_cache: dict, frontmatter_cache: dict -) -> dict[str, Any]: - """Extract metadata from document with caching.""" - if docname in metadata_cache: - return metadata_cache[docname] - - metadata = {} - - try: - if hasattr(env, "metadata") and docname in env.metadata: - metadata.update(env.metadata[docname]) - - source_path = env.doc2path(docname) - if source_path and str(source_path).endswith(".md"): - frontmatter = extract_frontmatter(str(source_path), frontmatter_cache) - if frontmatter: - metadata.update(frontmatter) - - metadata_cache[docname] = metadata - logger.debug(f"Successfully extracted metadata for {docname}: {len(metadata)} items") - - except Exception as e: # noqa: BLE001 - logger.warning(f"Error extracting metadata from {docname}: {e}") - metadata_cache[docname] = {} - - return metadata_cache[docname] - - -def extract_frontmatter(file_path: str, frontmatter_cache: dict) -> dict[str, Any] | None: - """Extract YAML frontmatter from markdown files.""" - if file_path in frontmatter_cache: - return frontmatter_cache[file_path] - - result = None - - # Check prerequisites - if not YAML_AVAILABLE: - logger.debug("PyYAML not available, skipping frontmatter extraction") - else: - try: - with open(file_path, encoding="utf-8") as f: - content = f.read() - - # Check for valid frontmatter format - if content.startswith("---"): - end_marker = content.find("\n---\n", 3) - if end_marker != -1: - frontmatter_text = content[3:end_marker] - result = yaml.safe_load(frontmatter_text) - - except yaml.YAMLError as e: - logger.warning(f"YAML parsing error in frontmatter for {file_path}: {e}") - result = None - except Exception as e: # noqa: BLE001 - logger.debug(f"Could not extract frontmatter from {file_path}: {e}") - result = None - - frontmatter_cache[file_path] = result - return result diff --git a/docs/_ext/json_output/content/structured.py b/docs/_ext/json_output/content/structured.py deleted file mode 100644 index 413810fc5..000000000 --- a/docs/_ext/json_output/content/structured.py +++ /dev/null @@ -1,399 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Structured content extraction functions for headings, code blocks, links, and images.""" - -import re -from typing import TYPE_CHECKING, Any - -from docutils import nodes -from sphinx import addnodes -from sphinx.util import logging - -if TYPE_CHECKING: - from sphinx.environment import BuildEnvironment - -logger = logging.getLogger(__name__) - - -def extract_headings(doctree: nodes.document) -> list[dict[str, Any]]: - """Extract headings from document tree.""" - headings = [] - - # Extract headings from section nodes - for node in doctree.traverse(nodes.section): - # Get the title node - title_node = node.next_node(nodes.title) - if title_node: - title_text = title_node.astext().strip() - if title_text: - # Determine heading level based on nesting - level = 1 - parent = node.parent - while parent and isinstance(parent, nodes.section): - level += 1 - parent = parent.parent - - # Generate ID (similar to how Sphinx does it) - heading_id = re.sub(r"[^\w\-_]", "", title_text.lower().replace(" ", "-")) - - headings.append({"text": title_text, "level": level, "id": heading_id}) - - # Also check for standalone title nodes (like document title) - for node in doctree.traverse(nodes.title): - if node.parent and not isinstance(node.parent, nodes.section): - title_text = node.astext().strip() - if title_text: - heading_id = re.sub(r"[^\w\-_]", "", title_text.lower().replace(" ", "-")) - headings.append({"text": title_text, "level": 1, "id": heading_id}) - - # Remove duplicates while preserving order - seen = set() - unique_headings = [] - for heading in headings: - heading_key = (heading["text"], heading["level"]) - if heading_key not in seen: - seen.add(heading_key) - unique_headings.append(heading) - - return unique_headings - - -def extract_code_blocks(doctree: nodes.document) -> list[dict[str, Any]]: - """Extract code blocks from document tree.""" - code_blocks = [] - - for node in doctree.traverse(nodes.literal_block): - code_content = node.astext().strip() - if code_content: - # Try to determine language from classes or attributes - language = "text" # default - - if hasattr(node, "attributes") and "classes" in node.attributes: - classes = node.attributes["classes"] - for cls in classes: - if cls.startswith("language-"): - language = cls[9:] # Remove 'language-' prefix - break - elif cls in [ - "python", - "bash", - "javascript", - "json", - "yaml", - "sql", - "html", - "css", - "cpp", - "c", - "java", - "rust", - "go", - ]: - language = cls - break - - # Also check for highlight language - if hasattr(node, "attributes") and "highlight_args" in node.attributes: - highlight_args = node.attributes["highlight_args"] - if "language" in highlight_args: - language = highlight_args["language"] - - code_blocks.append({"content": code_content, "language": language}) - - return code_blocks - - -def extract_links( - doctree: nodes.document, - env: "BuildEnvironment | None" = None, - docname: str = "", -) -> list[dict[str, Any]]: - """Extract links from document tree with enhanced metadata. - - Args: - doctree: The document tree to extract links from - env: Optional Sphinx build environment for title resolution - docname: Current document name for relative URL resolution - - Returns: - List of link dictionaries with text, url, type, and optional metadata - """ - links = [] - - # Extract standard reference nodes - for node in doctree.traverse(nodes.reference): - link = _extract_reference_node(node, env, docname) - if link: - links.append(link) - - # Extract download reference nodes - for node in doctree.traverse(addnodes.download_reference): - link = _extract_download_reference(node) - if link: - links.append(link) - - return links - - -def _extract_reference_node( - node: nodes.reference, - env: "BuildEnvironment | None", - current_docname: str, -) -> dict[str, Any] | None: - """Extract metadata from a reference node.""" - link_text = node.astext().strip() - if not link_text: - return None - - attrs = getattr(node, "attributes", {}) - link: dict[str, Any] = {"text": link_text, "type": "internal"} - - # Extract URL from various attributes - if "refuri" in attrs: - link["url"] = attrs["refuri"] - # Classify link type - if attrs["refuri"].startswith(("http://", "https://", "ftp://", "mailto:")): - link["type"] = "external" - elif attrs["refuri"].startswith("#"): - link["type"] = "anchor" - else: - link["type"] = "internal" - # Normalize internal URLs - link["url"] = _normalize_internal_url(attrs["refuri"], current_docname) - elif "refid" in attrs: - link["url"] = f"#{attrs['refid']}" - link["type"] = "anchor" - elif "reftarget" in attrs: - link["url"] = attrs["reftarget"] - link["type"] = "internal" - - # Extract cross-reference metadata (from :ref:, :doc:, {ref}, {doc}, etc.) - if "refdoc" in attrs: - link["target_doc"] = attrs["refdoc"] - if link["type"] == "internal": - link["type"] = "cross_reference" - - if "reftype" in attrs: - link["ref_type"] = attrs["reftype"] - - # Try to improve link text if it looks like a filename - if env and _looks_like_filename(link_text): - better_text = _resolve_link_text(link_text, attrs, env) - if better_text and better_text != link_text: - link["text"] = better_text - link["original_text"] = link_text # Keep original for debugging - - # Only return if we have a URL or target_doc - if link.get("url") or link.get("target_doc"): - return link - return None - - -def _extract_download_reference(node: addnodes.download_reference) -> dict[str, Any] | None: - """Extract metadata from a download reference node.""" - link_text = node.astext().strip() - attrs = getattr(node, "attributes", {}) - - if not link_text: - return None - - link: dict[str, Any] = { - "text": link_text, - "type": "download", - } - - if "reftarget" in attrs: - link["url"] = attrs["reftarget"] - if "filename" in attrs: - link["filename"] = attrs["filename"] - - return link if link.get("url") else None - - -def _normalize_internal_url(url: str, current_docname: str) -> str: - """Normalize internal URLs to consistent format. - - Converts .md/.rst extensions to .html and resolves relative paths. - """ - if not url: - return url - - # Already absolute or external - if url.startswith(("/", "http://", "https://", "#")): - # Just normalize extension for absolute internal paths - if url.startswith("/"): - return _normalize_extension(url) - return url - - # Relative URL - resolve against current document - if current_docname: - # Get directory of current document - if "/" in current_docname: - base_dir = current_docname.rsplit("/", 1)[0] - url = f"{base_dir}/{url}" - - return _normalize_extension(url) - - -def _normalize_extension(url: str) -> str: - """Normalize file extensions to .html.""" - # Split off anchor if present - anchor = "" - if "#" in url: - url, anchor = url.rsplit("#", 1) - anchor = f"#{anchor}" - - # Replace source extensions with .html - for ext in (".md", ".rst", ".txt"): - if url.endswith(ext): - url = url[: -len(ext)] + ".html" - break - - # Add .html if no extension - if url and not url.endswith(".html") and "." not in url.rsplit("/", 1)[-1]: - url = url + ".html" - - return url + anchor - - -def _looks_like_filename(text: str) -> bool: - """Check if text looks like a filename/docname rather than readable text.""" - if not text: - return False - - # Single word with no spaces, possibly with path separators - if " " not in text and ("/" in text or text == text.lower()): - # But not if it's a reasonable title-like word - if len(text) > 2 and text[0].isupper() and text[1:].islower(): - return False - return True - - # Contains path separators - if "/" in text or "\\" in text: - return True - - # Ends with file extension - if re.search(r"\.(md|rst|html|txt)$", text, re.IGNORECASE): - return True - - return False - - -def _resolve_link_text( - text: str, - attrs: dict[str, Any], - env: "BuildEnvironment", -) -> str: - """Try to resolve a filename-like link text to a proper title.""" - # Try to get the target document name - target_doc = attrs.get("refdoc") or attrs.get("reftarget", "") - - # Clean up the target - target_doc = target_doc.replace(".html", "").replace(".md", "").replace(".rst", "") - - if target_doc and hasattr(env, "titles") and target_doc in env.titles: - title_node = env.titles[target_doc] - if title_node: - return title_node.astext().strip() - - # Fallback: humanize the filename - return _humanize_filename(text) - - -def _humanize_filename(filename: str) -> str: - """Convert a filename to human-readable text.""" - # Get just the filename part - if "/" in filename: - filename = filename.rsplit("/", 1)[-1] - - # Remove extension - for ext in (".md", ".rst", ".html", ".txt"): - if filename.endswith(ext): - filename = filename[: -len(ext)] - break - - # Replace separators with spaces - filename = filename.replace("-", " ").replace("_", " ") - - # Title case - return filename.title() - - -def extract_images(doctree: nodes.document) -> list[dict[str, Any]]: - """Extract images from document tree.""" - images = [] - - # Extract standalone images - images.extend(_extract_standalone_images(doctree)) - - # Extract images within figures - images.extend(_extract_figure_images(doctree)) - - return images - - -def _extract_standalone_images(doctree: nodes.document) -> list[dict[str, Any]]: - """Extract standalone image nodes.""" - images = [] - - for node in doctree.traverse(nodes.image): - if hasattr(node, "attributes"): - image_info = _build_image_info(node.attributes) - if image_info: - images.append(image_info) - - return images - - -def _extract_figure_images(doctree: nodes.document) -> list[dict[str, Any]]: - """Extract images from figure nodes.""" - images = [] - - for node in doctree.traverse(nodes.figure): - for img_node in node.traverse(nodes.image): - if hasattr(img_node, "attributes"): - image_info = _build_image_info(img_node.attributes) - if image_info: - # Add caption from figure - caption = _extract_figure_caption(node) - if caption: - image_info["caption"] = caption - images.append(image_info) - - return images - - -def _build_image_info(attrs: dict[str, Any]) -> dict[str, Any] | None: - """Build image info dictionary from attributes.""" - image_src = attrs.get("uri", "") - if not image_src: - return None - - image_info = {"src": image_src, "alt": attrs.get("alt", "")} - - # Add optional attributes - for attr_name in ["title", "width", "height"]: - if attr_name in attrs: - image_info[attr_name] = attrs[attr_name] - - return image_info - - -def _extract_figure_caption(figure_node: nodes.figure) -> str: - """Extract caption text from figure node.""" - for caption_node in figure_node.traverse(nodes.caption): - return caption_node.astext().strip() - return "" diff --git a/docs/_ext/json_output/content/text.py b/docs/_ext/json_output/content/text.py deleted file mode 100644 index 6e91afe71..000000000 --- a/docs/_ext/json_output/content/text.py +++ /dev/null @@ -1,372 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Text content extraction functions.""" - -import re -from typing import Any - -from docutils import nodes -from sphinx.environment import BuildEnvironment -from sphinx.util import logging - -logger = logging.getLogger(__name__) - -# Constants -MIN_SUBSTANTIAL_CONTENT_LENGTH = 50 -MAX_SUMMARY_LENGTH = 300 -MIN_KEYWORD_LENGTH = 3 -MAX_KEYWORDS_RETURNED = 50 - - -def extract_raw_markdown(env: BuildEnvironment, docname: str) -> str | None: - """Extract raw markdown from source file.""" - try: - source_path = env.doc2path(docname) - if not source_path or not source_path.exists(): - return None - - with open(source_path, encoding="utf-8") as f: - content = f.read() - - # Remove frontmatter if present - if content.startswith("---"): - end_marker = content.find("\n---\n", 3) - if end_marker != -1: - content = content[end_marker + 5 :] # Skip the second ---\n - - return content.strip() - - except Exception as e: # noqa: BLE001 - logger.debug(f"Could not extract raw markdown from {docname}: {e}") - return None - - -def extract_text_content(doctree: nodes.document) -> str: - """Extract plain text content from document tree.""" - text_parts = [] - - for node in doctree.traverse(nodes.Text): - text_parts.append(node.astext()) - - return " ".join(text_parts).strip() - - -def extract_clean_text_content(doctree: nodes.document, env: BuildEnvironment | None = None) -> str: - """Extract clean text content, filtering out navigation elements. - - Args: - doctree: The document tree to extract text from - env: Optional Sphinx environment for resolving link titles - - Returns: - Cleaned text content suitable for search/LLM consumption - """ - text_parts = [] - # Track nodes we've already processed (to avoid duplicate text from references) - processed_refs = set() - - for node in doctree.traverse(): - # Skip certain node types that aren't content - if isinstance(node, (nodes.target, nodes.substitution_definition)): - continue - - # Skip toctree and other directive content - if hasattr(node, "tagname") and node.tagname in ["toctree", "index", "meta"]: - continue - - # Handle reference nodes specially - extract and potentially improve link text - if isinstance(node, nodes.reference): - ref_id = id(node) - if ref_id not in processed_refs: - processed_refs.add(ref_id) - link_text = _get_improved_link_text(node, env) - if link_text: - text_parts.append(link_text) - continue - - # Extract text from text nodes (but skip if inside a reference we already processed) - if isinstance(node, nodes.Text): - # Check if this text node is inside a reference - parent = node.parent - if isinstance(parent, nodes.reference) and id(parent) in processed_refs: - continue # Already handled by reference processing - - text = node.astext().strip() - if text and not text.startswith("¶"): # Skip permalink symbols - text_parts.append(text) - - # Join and clean up the text - full_text = " ".join(text_parts) - - # Clean up whitespace - full_text = re.sub(r"\s+", " ", full_text) - - return full_text.strip() - - -def _get_improved_link_text(node: nodes.reference, env: BuildEnvironment | None) -> str: - """Get improved link text, resolving filenames to titles where possible.""" - text = node.astext().strip() - if not text: - return "" - - # If text doesn't look like a filename, use it as-is - if not _text_looks_like_filename(text): - return text - - # Try to resolve to a better title - attrs = getattr(node, "attributes", {}) - - # Try refdoc first (target document for cross-references) - target_doc = attrs.get("refdoc", "") - - # Try reftarget as fallback - if not target_doc: - target_doc = attrs.get("reftarget", "") - # Clean up the target - target_doc = target_doc.replace(".html", "").replace(".md", "").replace(".rst", "") - - # Look up title in env.titles - if target_doc and env and hasattr(env, "titles") and target_doc in env.titles: - title_node = env.titles[target_doc] - if title_node: - resolved_title = title_node.astext().strip() - if resolved_title: - return resolved_title - - # Fallback: humanize the filename - return _humanize_link_text(text) - - -def _text_looks_like_filename(text: str) -> bool: - """Check if text looks like a filename rather than readable text.""" - if not text: - return False - - # Contains path separators - if "/" in text or "\\" in text: - return True - - # Ends with file extension - if re.search(r"\.(md|rst|html|txt)$", text, re.IGNORECASE): - return True - - # Single lowercase word (like "index", "readme", "configuration") - if " " not in text and text == text.lower() and len(text) > 2: - # But allow proper nouns that happen to be lowercase in context - return True - - return False - - -def _humanize_link_text(text: str) -> str: - """Convert filename-like text to human-readable form.""" - # Get just the filename part - if "/" in text: - text = text.rsplit("/", 1)[-1] - - # Remove extension - for ext in (".md", ".rst", ".html", ".txt"): - if text.endswith(ext): - text = text[: -len(ext)] - break - - # Replace separators with spaces - text = text.replace("-", " ").replace("_", " ") - - # Title case - return text.title() - - -def clean_text_for_llm(text: str) -> str: - """Clean text content to make it more suitable for LLM processing and search indexing.""" - if not text: - return "" - - # Remove SVG content (common in documentation) - text = re.sub(r"]*>.*?", "", text, flags=re.DOTALL | re.IGNORECASE) - - # Remove HTML comments - text = re.sub(r"", "", text, flags=re.DOTALL) - - # Remove empty directive blocks (common MyST artifacts) - text = re.sub(r"^\s*```\{[^}]+\}\s*```\s*$", "", text, flags=re.MULTILINE) - - # Remove toctree artifacts - text = re.sub(r"^\s*:caption:.*$", "", text, flags=re.MULTILINE) - text = re.sub(r"^\s*:hidden:\s*$", "", text, flags=re.MULTILINE) - text = re.sub(r"^\s*:glob:\s*$", "", text, flags=re.MULTILINE) - text = re.sub(r"^\s*:maxdepth:\s*\d+\s*$", "", text, flags=re.MULTILINE) - - # Remove common MyST directive markers that aren't useful for search - text = re.sub(r"^\s*:::\{[^}]+\}\s*$", "", text, flags=re.MULTILINE) - text = re.sub(r"^\s*:::\s*$", "", text, flags=re.MULTILINE) - - # Clean up code block language indicators - text = re.sub(r"```(\w+)\s*\n", "```\n", text) - - # Remove excessive whitespace but preserve paragraph breaks - text = re.sub(r"\n\s*\n\s*\n+", "\n\n", text) # Multiple line breaks -> double - text = re.sub(r"[ \t]+", " ", text) # Multiple spaces/tabs -> single space - - # Remove lines that are just punctuation or symbols - lines = text.split("\n") - cleaned_lines = [] - for line in lines: - stripped = line.strip() - # Keep line if it has actual words (not just punctuation/symbols) - if stripped and re.search(r"[a-zA-Z0-9]", stripped): - # Remove standalone punctuation at start/end - stripped = re.sub(r"^[^\w\s]+\s*", "", stripped) - stripped = re.sub(r"\s*[^\w\s]+$", "", stripped) - if stripped: - cleaned_lines.append(stripped) - - text = "\n".join(cleaned_lines) - - # Final cleanup - return text.strip() - - -def extract_directive_content(directive_block: str) -> str: - """Extract meaningful content from MyST directive blocks.""" - if not directive_block: - return "" - - # Remove the directive syntax but keep the content - lines = directive_block.split("\n") - content_lines = [] - in_content = False - - for line in lines: - # Skip directive header lines - if line.strip().startswith(":::") or line.strip().startswith("```{"): - in_content = True - continue - elif line.strip() == ":::" or line.strip() == "```": - continue - elif line.strip().startswith(":") and not in_content: - # Skip directive options - continue - - # Include content lines - if in_content or not line.strip().startswith(":"): - content_lines.append(line) - - return "\n".join(content_lines).strip() - - -def extract_summary(doctree: nodes.document) -> str: - """Extract a summary from the document (first paragraph or section).""" - # Try to find the first substantial paragraph - for node in doctree.traverse(nodes.paragraph): - text = node.astext().strip() - if text and len(text) > MIN_SUBSTANTIAL_CONTENT_LENGTH: # Substantial content - # Clean and truncate - text = re.sub(r"\s+", " ", text) - if len(text) > MAX_SUMMARY_LENGTH: - text = text[:297] + "..." - return text - - # Fallback: use first MAX_SUMMARY_LENGTH characters of any text - text = extract_text_content(doctree) - if text: - text = re.sub(r"\s+", " ", text) - if len(text) > MAX_SUMMARY_LENGTH: - text = text[:297] + "..." - return text - - return "" - - -def extract_keywords(content: str, headings: list[dict[str, Any]]) -> list[str]: - """Extract relevant keywords from content for search optimization.""" - if not content: - return [] - - keywords = set() - - # Add heading text as keywords - for heading in headings: - if "text" in heading: - # Split heading into words and add significant ones - words = re.findall(r"\b[a-zA-Z]{3,}\b", heading["text"].lower()) - keywords.update(words) - - # Extract technical terms (often capitalized or have specific patterns) - # API names, class names, function names, etc. - tech_terms = re.findall(r"\b[A-Z][a-zA-Z0-9_]*[a-z][a-zA-Z0-9_]*\b", content) - keywords.update(term.lower() for term in tech_terms) - - # Extract quoted terms (often important concepts) - quoted_terms = re.findall(r'["`]([^"`]{3,20})["`]', content) - for term in quoted_terms: - if re.match(r"^[a-zA-Z][a-zA-Z0-9_\-\s]*$", term): - keywords.add(term.lower().strip()) - - # Extract common patterns for documentation keywords - # Configuration keys, file extensions, command names - config_keys = re.findall(r"\b[a-z_]+[a-z0-9_]*\s*[:=]", content) - keywords.update(key.rstrip(":=").strip() for key in config_keys) - - # File extensions - extensions = re.findall(r"\.[a-z]{2,4}\b", content.lower()) - keywords.update(ext.lstrip(".") for ext in extensions) - - # Remove common stop words and very short terms - stop_words = { - "the", - "and", - "for", - "are", - "but", - "not", - "you", - "all", - "can", - "had", - "her", - "was", - "one", - "our", - "out", - "day", - "get", - "has", - "him", - "his", - "how", - "its", - "may", - "new", - "now", - "old", - "see", - "two", - "who", - "boy", - "did", - "she", - "use", - "way", - "what", - "when", - "will", - } - keywords = {kw for kw in keywords if len(kw) >= MIN_KEYWORD_LENGTH and kw not in stop_words} - - # Return sorted list, limited to reasonable number - return sorted(keywords)[:MAX_KEYWORDS_RETURNED] diff --git a/docs/_ext/json_output/core/__init__.py b/docs/_ext/json_output/core/__init__.py deleted file mode 100644 index b1512c113..000000000 --- a/docs/_ext/json_output/core/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Core JSON output generation components.""" - -from .builder import JSONOutputBuilder -from .document_discovery import DocumentDiscovery -from .global_metadata import get_global_metadata -from .hierarchy_builder import HierarchyBuilder -from .json_formatter import JSONFormatter -from .json_writer import JSONWriter - -__all__ = [ - "DocumentDiscovery", - "HierarchyBuilder", - "JSONFormatter", - "JSONOutputBuilder", - "JSONWriter", - "get_global_metadata", -] diff --git a/docs/_ext/json_output/core/builder.py b/docs/_ext/json_output/core/builder.py deleted file mode 100644 index 2652b9493..000000000 --- a/docs/_ext/json_output/core/builder.py +++ /dev/null @@ -1,110 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""JSONOutputBuilder class for handling JSON output generation.""" - -from typing import Any - -from sphinx.application import Sphinx -from sphinx.util import logging - -from ..content import extract_document_content as _extract_document_content -from ..content import extract_document_metadata as _extract_document_metadata -from ..processing.cache import JSONOutputCache -from ..utils import get_setting, should_generate_json -from .document_discovery import DocumentDiscovery -from .hierarchy_builder import HierarchyBuilder -from .json_formatter import JSONFormatter -from .json_writer import JSONWriter - -logger = logging.getLogger(__name__) - - -class JSONOutputBuilder: - """Handles JSON output generation for documents.""" - - def __init__(self, app: Sphinx): - self.app = app - self.env = app.env - self.config = app.config - - # Initialize cache manager - self.cache = JSONOutputCache() - - # Initialize modular components - self.document_discovery = DocumentDiscovery(app, self) - self.json_formatter = JSONFormatter(app, self) - self.json_writer = JSONWriter(app) - self.hierarchy_builder = HierarchyBuilder(app, self, self.document_discovery, self.json_formatter) - - def should_generate_json(self, docname: str) -> bool: - """Check if JSON should be generated for this document.""" - return should_generate_json(self.config, docname) - - def needs_update(self, docname: str) -> bool: - """Check if document needs to be updated based on modification time.""" - incremental_enabled = get_setting(self.config, "incremental_build", False) - source_path = self.env.doc2path(docname) - return self.cache.needs_update(docname, source_path, incremental_enabled) - - def mark_updated(self, docname: str) -> None: - """Mark document as processed with current timestamp.""" - source_path = self.env.doc2path(docname) - self.cache.mark_updated(docname, source_path) - - def extract_document_metadata(self, docname: str) -> dict[str, Any]: - """Extract metadata from document with caching.""" - return self.cache.with_cache_lock( - _extract_document_metadata, - self.env, - docname, - self.cache.get_metadata_cache(), - self.cache.get_frontmatter_cache(), - ) - - def extract_document_content(self, docname: str) -> dict[str, Any]: - """Extract content from document optimized for LLM/search use cases.""" - return self.cache.with_cache_lock(_extract_document_content, self.env, docname, self.cache.get_content_cache()) - - def build_json_data(self, docname: str) -> dict[str, Any]: - """Build optimized JSON data structure for LLM/search use cases.""" - # Use the JSON formatter for base data - data = self.json_formatter.build_json_data(docname) - - # Add children for directory indexes using hierarchy builder - self.hierarchy_builder.add_children_to_data(data, docname) - - return data - - def write_json_file(self, docname: str, data: dict[str, Any]) -> None: - """Write JSON data to file.""" - self.json_writer.write_json_file(docname, data) - - # Delegate methods to maintain API compatibility - def get_child_documents(self, parent_docname: str) -> list[str]: - """Get all child documents for a parent directory.""" - return self.document_discovery.get_child_documents(parent_docname) - - def is_hidden_document(self, docname: str) -> bool: - """Check if a document should be considered hidden.""" - return self.document_discovery.is_hidden_document(docname) - - def get_all_documents_recursive(self) -> list[str]: - """Get all non-hidden documents recursively.""" - return self.document_discovery.get_all_documents_recursive() - - def build_child_json_data(self, docname: str, include_content: bool | None = None) -> dict[str, Any]: - """Build optimized JSON data for child documents (LLM/search focused).""" - return self.json_formatter.build_child_json_data(docname, include_content) diff --git a/docs/_ext/json_output/core/document_discovery.py b/docs/_ext/json_output/core/document_discovery.py deleted file mode 100644 index 3dc255ba8..000000000 --- a/docs/_ext/json_output/core/document_discovery.py +++ /dev/null @@ -1,130 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Document discovery and filtering functionality.""" - -from typing import TYPE_CHECKING - -from sphinx.application import Sphinx - -from ..utils import get_setting - -if TYPE_CHECKING: - from .builder import JSONOutputBuilder - - -class DocumentDiscovery: - """Handles document discovery, filtering, and hierarchical relationships.""" - - def __init__(self, app: Sphinx, json_builder: "JSONOutputBuilder"): - self.app = app - self.env = app.env - self.config = app.config - self.json_builder = json_builder # Reference to main builder for metadata access - - def get_child_documents(self, parent_docname: str) -> list[str]: - """Get all child documents for a parent directory.""" - if parent_docname == "index": - parent_path = "" - elif parent_docname.endswith("/index"): - parent_path = parent_docname[:-6] # Remove '/index' - else: - # Not a directory index, no children - return [] - - children = [] - for docname in self.env.all_docs: - if self.is_hidden_document(docname): - continue - - # Skip the parent itself - if docname == parent_docname: - continue - - # Check if this document is a child of the parent - if parent_path == "": - # Root level - include all docs - children.append(docname) - elif docname.startswith(parent_path + "/"): - children.append(docname) - - return sorted(children) - - def is_hidden_document(self, docname: str) -> bool: - """Check if a document should be considered hidden.""" - # Skip documents that match exclude patterns - for pattern in get_setting(self.config, "exclude_patterns", []): - if docname.startswith(pattern): - return True - - # Skip documents with 'hidden' or 'draft' in metadata - metadata = self.json_builder.extract_document_metadata(docname) - if metadata.get("hidden") or metadata.get("draft"): - return True - - # Skip documents that wouldn't generate JSON - return not self.json_builder.should_generate_json(docname) - - def get_all_documents_recursive(self) -> list[str]: - """Get all non-hidden documents recursively.""" - all_docs = [] - for docname in self.env.all_docs: - if not self.is_hidden_document(docname): - all_docs.append(docname) - return sorted(all_docs) - - def get_section_path(self, docname: str) -> list[str]: - """Get hierarchical section path for navigation.""" - parts = docname.split("/") - - # Filter out common file names to get clean section path - filtered_parts = [] - for part in parts: - if part not in ["index", "README"]: - filtered_parts.append(part.replace("-", " ").replace("_", " ").title()) - - return filtered_parts - - def detect_document_type(self, docname: str, title: str, content: str) -> str: - """Detect document type for better search categorization.""" - docname_lower = docname.lower() - title_lower = title.lower() - content_lower = content.lower()[:1000] # First 1000 chars - - # Define document type checks in priority order - type_checks = [ - ("tutorial", lambda: "tutorial" in docname_lower or "tutorial" in title_lower), - ("guide", lambda: "guide" in docname_lower or "guide" in title_lower), - ("reference", lambda: "reference" in docname_lower or "api" in docname_lower), - ("example", lambda: "example" in docname_lower or "examples" in docname_lower), - ("troubleshooting", lambda: "troubleshoot" in docname_lower or "faq" in docname_lower), - ("installation", lambda: "install" in docname_lower or "setup" in docname_lower), - ("overview", lambda: docname.endswith("/index")), - ( - "tutorial", - lambda: any(word in content_lower for word in ["$ ", "pip install", "docker run", "git clone"]), - ), - ( - "reference", - lambda: any(word in content_lower for word in ["class ", "def ", "function", "method", "parameter"]), - ), - ] - - # Check each type in order and return the first match - for doc_type, check_func in type_checks: - if check_func(): - return doc_type - - return "documentation" diff --git a/docs/_ext/json_output/core/global_metadata.py b/docs/_ext/json_output/core/global_metadata.py deleted file mode 100644 index 83430afd2..000000000 --- a/docs/_ext/json_output/core/global_metadata.py +++ /dev/null @@ -1,155 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Global metadata extraction from Sphinx configuration. - -This module provides functions to extract and build global metadata -from conf.py settings for inclusion in JSON output files. -""" - -import re -from typing import Any - -from sphinx.config import Config -from sphinx.util import logging - -logger = logging.getLogger(__name__) - - -def get_global_metadata(config: Config) -> dict[str, Any]: - """Build global metadata from Sphinx config settings. - - Combines explicit global_metadata settings with auto-inferred values - from standard Sphinx configuration (project, release, etc.). - - Args: - config: Sphinx configuration object - - Returns: - Dictionary with global metadata (book, product, site sections) - """ - settings = getattr(config, "json_output_settings", {}) - - # Start with explicit global_metadata if provided - global_meta = _deep_copy_dict(settings.get("global_metadata", {})) - - # Auto-infer if enabled - if settings.get("infer_global_metadata", True): - _infer_book_metadata(global_meta, config) - _infer_product_metadata(global_meta, config) - _infer_site_metadata(global_meta, config) - - # Remove empty sections - return {k: v for k, v in global_meta.items() if v} - - -def _deep_copy_dict(d: dict) -> dict: - """Create a deep copy of a nested dictionary.""" - result = {} - for k, v in d.items(): - if isinstance(v, dict): - result[k] = _deep_copy_dict(v) - elif isinstance(v, list): - result[k] = list(v) - else: - result[k] = v - return result - - -def _infer_book_metadata(global_meta: dict, config: Config) -> None: - """Infer book metadata from Sphinx config.""" - global_meta.setdefault("book", {}) - book = global_meta["book"] - - # book.title from project - if "title" not in book and hasattr(config, "project"): - book["title"] = config.project - - # book.version from release - if "version" not in book and hasattr(config, "release"): - book["version"] = config.release - - -def _infer_product_metadata(global_meta: dict, config: Config) -> None: - """Infer product metadata from Sphinx config.""" - global_meta.setdefault("product", {}) - product = global_meta["product"] - - # Try to get from html_context first (explicit config) - html_context = getattr(config, "html_context", {}) - - # product.name - if "name" not in product: - if html_context.get("product_name"): - product["name"] = html_context["product_name"] - elif hasattr(config, "project"): - product["name"] = _extract_product_name(config.project) - - # product.family - if "family" not in product and html_context.get("product_family"): - family = html_context["product_family"] - product["family"] = family if isinstance(family, list) else [family] - - # product.version (can differ from book.version) - if "version" not in product and hasattr(config, "release"): - product["version"] = config.release - - -def _infer_site_metadata(global_meta: dict, config: Config) -> None: - """Infer site metadata from Sphinx config.""" - html_context = getattr(config, "html_context", {}) - - # Only add site section if we have data - site_name = html_context.get("site_name") - if site_name: - global_meta.setdefault("site", {}) - if "name" not in global_meta["site"]: - global_meta["site"]["name"] = site_name - - -def _extract_product_name(project: str) -> str: - """Extract product name from project string. - - Examples: - 'NVIDIA DORI' -> 'DORI' - 'NVIDIA NeMo Curator User Guide' -> 'NeMo Curator' - 'NeMo Framework Documentation' -> 'NeMo Framework' - - Args: - project: The Sphinx project name - - Returns: - Extracted product name - """ - name = project - - # Remove NVIDIA prefix - name = re.sub(r"^NVIDIA\s+", "", name, flags=re.IGNORECASE) - - # Remove common documentation suffixes - suffixes = [ - r"\s+User Guide$", - r"\s+User Manual$", - r"\s+Developer Guide$", - r"\s+Documentation$", - r"\s+Reference$", - r"\s+Reference Guide$", - r"\s+API Reference$", - r"\s+Docs$", - ] - for suffix in suffixes: - name = re.sub(suffix, "", name, flags=re.IGNORECASE) - - return name.strip() diff --git a/docs/_ext/json_output/core/hierarchy_builder.py b/docs/_ext/json_output/core/hierarchy_builder.py deleted file mode 100644 index 8bd53c135..000000000 --- a/docs/_ext/json_output/core/hierarchy_builder.py +++ /dev/null @@ -1,140 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Hierarchy building for complex document structures like main index.""" - -from typing import TYPE_CHECKING, Any - -from sphinx.application import Sphinx -from sphinx.util import logging - -from ..utils import get_setting - -if TYPE_CHECKING: - from .builder import JSONOutputBuilder - from .document_discovery import DocumentDiscovery - from .json_formatter import JSONFormatter - -logger = logging.getLogger(__name__) - - -class HierarchyBuilder: - """Handles complex hierarchy building for indexes.""" - - def __init__( - self, - app: Sphinx, - json_builder: "JSONOutputBuilder", - document_discovery: "DocumentDiscovery", - json_formatter: "JSONFormatter", - ): - self.app = app - self.config = app.config - self.json_builder = json_builder - self.document_discovery = document_discovery - self.json_formatter = json_formatter - - def add_children_to_data(self, data: dict[str, Any], docname: str) -> None: - """Add children documents to data structure for directory indexes.""" - include_children = get_setting(self.config, "include_children", True) - if not include_children or not (docname == "index" or docname.endswith("/index")): - return - - if docname == "index": - self._handle_main_index(data, docname) - else: - self._handle_directory_index(data, docname) - - def _handle_main_index(self, data: dict[str, Any], docname: str) -> None: - """Handle main index behavior: optimized for search index generation.""" - main_index_mode = get_setting(self.config, "main_index_mode", "full") - max_main_index_docs = get_setting(self.config, "max_main_index_docs", 1000) - - if main_index_mode == "disabled": - logger.info("Main index children disabled by configuration") - data["children"] = [] - data["total_documents"] = 0 - elif main_index_mode == "metadata_only": - self._build_metadata_only_index(data, docname, max_main_index_docs) - else: # 'full' mode - comprehensive search index - self._build_full_search_index(data, docname, max_main_index_docs) - - def _build_metadata_only_index(self, data: dict[str, Any], docname: str, max_docs: int) -> None: - """Build metadata-only search index for main index page.""" - logger.info("Building metadata-only search index for main index page...") - all_docs = self.document_discovery.get_all_documents_recursive() - - # Apply document limit if set (0 = no limit) - if max_docs > 0: - all_docs = all_docs[:max_docs] - if len(self.document_discovery.get_all_documents_recursive()) > max_docs: - logger.info(f"Limited to {max_docs} documents (set max_main_index_docs to 0 for no limit)") - - # Build flat array of documents for search index - documents = [] - for child_docname in all_docs: - if child_docname != docname: # Don't include self - try: - child_data = self.json_formatter.build_child_json_data(child_docname, include_content=False) - documents.append(child_data) - except Exception as e: # noqa: BLE001 - logger.warning(f"Failed to build child metadata for {child_docname}: {e}") - - # Store as flat array - will be output as array at root level - data["_documents_array"] = documents - data["total_documents"] = len(self.document_discovery.get_all_documents_recursive()) - - logger.info(f"Generated metadata-only search index with {len(documents)} documents") - - def _build_full_search_index(self, data: dict[str, Any], docname: str, max_docs: int) -> None: - """Build comprehensive search index for main index page.""" - logger.info("Building comprehensive search index for main index page...") - all_docs = self.document_discovery.get_all_documents_recursive() - - # Apply document limit if set (0 = no limit) - if max_docs > 0: - all_docs = all_docs[:max_docs] - if len(self.document_discovery.get_all_documents_recursive()) > max_docs: - logger.info(f"Limited to {max_docs} documents (set max_main_index_docs to 0 for no limit)") - - # Build flat array of documents for search index - documents = [] - for child_docname in all_docs: - if child_docname != docname: # Don't include self - try: - child_data = self.json_formatter.build_child_json_data(child_docname) - documents.append(child_data) - except Exception as e: # noqa: BLE001 - logger.warning(f"Failed to build child data for {child_docname}: {e}") - - # Store as flat array - will be output as array at root level - data["_documents_array"] = documents - data["total_documents"] = len(self.document_discovery.get_all_documents_recursive()) - - logger.info(f"Generated comprehensive search index with {len(documents)} documents") - - def _handle_directory_index(self, data: dict[str, Any], docname: str) -> None: - """Handle directory index: gets direct children.""" - children = self.document_discovery.get_child_documents(docname) - data["children"] = [] - - for child_docname in children: - try: - child_data = self.json_formatter.build_child_json_data(child_docname) - data["children"].append(child_data) - except Exception as e: # noqa: BLE001, PERF203 - logger.warning(f"Failed to build child data for {child_docname}: {e}") - - logger.debug(f"Included {len(data['children'])} child documents for {docname}") diff --git a/docs/_ext/json_output/core/json_formatter.py b/docs/_ext/json_output/core/json_formatter.py deleted file mode 100644 index 250451a31..000000000 --- a/docs/_ext/json_output/core/json_formatter.py +++ /dev/null @@ -1,278 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""JSON data formatting and structure building.""" - -from datetime import datetime, timezone -from typing import TYPE_CHECKING, Any - -from docutils import nodes -from sphinx.application import Sphinx -from sphinx.util import logging - -from ..utils import get_document_url, get_setting -from .document_discovery import DocumentDiscovery -from .global_metadata import get_global_metadata - -if TYPE_CHECKING: - from .builder import JSONOutputBuilder - -logger = logging.getLogger(__name__) - - -class JSONFormatter: - """Handles JSON data structure building and formatting.""" - - def __init__(self, app: Sphinx, json_builder: "JSONOutputBuilder"): - self.app = app - self.env = app.env - self.config = app.config - self.json_builder = json_builder - self._global_metadata: dict[str, Any] | None = None - - @property - def global_metadata(self) -> dict[str, Any]: - """Get cached global metadata from conf.py.""" - if self._global_metadata is None: - self._global_metadata = get_global_metadata(self.config) - return self._global_metadata - - def add_metadata_fields(self, data: dict[str, Any], metadata: dict[str, Any]) -> None: - """Add all metadata fields to JSON data structure. - - Supports both new nested schema and legacy flat fields for backwards compatibility. - New schema: topics, tags, industry, content.type, content.learning_level, content.audience, facets.modality - Legacy schema: categories, personas, difficulty, content_type, modality - """ - # Basic metadata fields - if metadata.get("description"): - data["description"] = metadata["description"] - - # Tags (same in both schemas) - if metadata.get("tags"): - data["tags"] = metadata["tags"] if isinstance(metadata["tags"], list) else [metadata["tags"]] - - # Topics (new schema) or categories (legacy) - topics = metadata.get("topics") or metadata.get("categories") - if topics: - data["topics"] = topics if isinstance(topics, list) else [topics] - - # Industry verticals - if metadata.get("industry"): - industry = metadata["industry"] - data["industry"] = industry if isinstance(industry, list) else [industry] - - if metadata.get("author"): - data["author"] = metadata["author"] - - # Content classification - support nested and flat structures - content = metadata.get("content", {}) - - # Content type: content.type (new) or content_type (legacy) - content_type = content.get("type") if isinstance(content, dict) else None - content_type = content_type or metadata.get("content_type") - if content_type: - data["content_type"] = content_type - - # Learning level: content.learning_level (new) or content.difficulty/difficulty (legacy) - learning_level = content.get("learning_level") if isinstance(content, dict) else None - learning_level = learning_level or content.get("difficulty") if isinstance(content, dict) else None - learning_level = learning_level or metadata.get("learning_level") or metadata.get("difficulty") - if learning_level: - data["learning_level"] = learning_level - - # Audience: content.audience (new) or personas (legacy) - audience = content.get("audience") if isinstance(content, dict) else None - audience = audience or metadata.get("personas") - if audience: - data["audience"] = audience if isinstance(audience, list) else [audience] - - # Keywords from frontmatter (takes priority over auto-extraction) - if metadata.get("keywords"): - keywords = metadata["keywords"] - data["keywords"] = keywords if isinstance(keywords, list) else [keywords] - - # Product-specific facets - dynamically extract all facet keys - facets = metadata.get("facets", {}) - if isinstance(facets, dict) and facets: - # Include all facets as a nested object - data["facets"] = facets - # Also flatten facets to top level for backwards compatibility and easier filtering - for facet_key, facet_value in facets.items(): - data[facet_key] = facet_value - - # Legacy flat modality support (if not already set via facets) - if "modality" not in data and metadata.get("modality"): - data["modality"] = metadata["modality"] - - # Content gating - if metadata.get("only"): - data["only"] = metadata["only"] - - def build_child_json_data(self, docname: str, include_content: bool | None = None) -> dict[str, Any]: - """Build optimized JSON data for child documents (LLM/search focused).""" - if include_content is None: - include_content = get_setting(self.config, "include_child_content", True) - - # Get document title - title = self.env.titles.get(docname, nodes.title()).astext() if docname in self.env.titles else "" - - # Extract metadata for tags/categories - metadata = self.json_builder.extract_document_metadata(docname) - content_data = self.json_builder.extract_document_content(docname) if include_content else {} - - # Build optimized data structure for search engines - data = { - "id": docname, # Use 'id' for search engines - "title": title, - "url": get_document_url(self.app, docname), - } - - # Add global metadata from conf.py (book, product, site) - self._add_global_metadata(data) - - # Add metadata fields from frontmatter - self.add_metadata_fields(data, metadata) - - # Add search-specific fields - if include_content: - self._add_content_fields(data, content_data, docname, title) - - return data - - def build_json_data(self, docname: str) -> dict[str, Any]: - """Build optimized JSON data structure for LLM/search use cases.""" - # Get document title - title = self.env.titles.get(docname, nodes.title()).astext() if docname in self.env.titles else "" - - # Extract metadata and content - metadata = self.json_builder.extract_document_metadata(docname) - content_data = self.json_builder.extract_document_content(docname) - - # Build data structure - data = { - "id": docname, - "title": title, - "url": get_document_url(self.app, docname), - "last_modified": datetime.now(timezone.utc).isoformat(), - } - - # Add global metadata from conf.py (book, product, site) - self._add_global_metadata(data) - - # Add metadata fields from frontmatter - self.add_metadata_fields(data, metadata) - - # Add content - if content_data.get("content"): - data["content"] = content_data["content"] - data["format"] = content_data.get("format", "text") - - if content_data.get("summary"): - data["summary"] = content_data["summary"] - - if content_data.get("headings"): - data["headings"] = [{"text": h["text"], "level": h["level"]} for h in content_data["headings"]] - - return data - - def _add_global_metadata(self, data: dict[str, Any]) -> None: - """Inject global site/book/product metadata from conf.py.""" - for key, value in self.global_metadata.items(): - if value: # Only add non-empty values - data[key] = value - - def _add_content_fields(self, data: dict[str, Any], content_data: dict[str, Any], docname: str, title: str) -> None: - """Add content-related fields to JSON data.""" - self._add_primary_content(data, content_data) - self._add_summary_content(data, content_data) - self._add_headings_content(data, content_data) - self._add_optional_features(data, content_data) - self._add_document_metadata(data, content_data, docname, title) - - def _add_primary_content(self, data: dict[str, Any], content_data: dict[str, Any]) -> None: - """Add primary content with length limits.""" - if not content_data.get("content"): - return - - content_max_length = get_setting(self.config, "content_max_length", 50000) - content = content_data["content"] - - if content_max_length > 0 and len(content) > content_max_length: - content = content[:content_max_length] + "..." - - data["content"] = content - data["format"] = content_data.get("format", "text") - data["content_length"] = len(content_data["content"]) # Original length - data["word_count"] = len(content_data["content"].split()) if content_data["content"] else 0 - - def _add_summary_content(self, data: dict[str, Any], content_data: dict[str, Any]) -> None: - """Add summary with length limits.""" - if not content_data.get("summary"): - return - - summary_max_length = get_setting(self.config, "summary_max_length", 500) - summary = content_data["summary"] - - if summary_max_length > 0 and len(summary) > summary_max_length: - summary = summary[:summary_max_length] + "..." - - data["summary"] = summary - - def _add_headings_content(self, data: dict[str, Any], content_data: dict[str, Any]) -> None: - """Add headings for structure/navigation.""" - if not content_data.get("headings"): - return - - # Simplify headings for LLM use - data["headings"] = [ - {"text": h["text"], "level": h["level"], "id": h.get("id", "")} for h in content_data["headings"] - ] - # Add searchable heading text - data["headings_text"] = " ".join([h["text"] for h in content_data["headings"]]) - - def _add_optional_features(self, data: dict[str, Any], content_data: dict[str, Any]) -> None: - """Add optional search enhancement features.""" - # Keywords: frontmatter takes priority, then auto-extraction - if "keywords" not in data: # Not already set from frontmatter - if get_setting(self.config, "extract_keywords", True) and "keywords" in content_data: - keywords_max_count = get_setting(self.config, "keywords_max_count", 50) - keywords = ( - content_data["keywords"][:keywords_max_count] - if keywords_max_count > 0 - else content_data["keywords"] - ) - data["keywords"] = keywords - - if get_setting(self.config, "extract_code_blocks", True) and "code_blocks" in content_data: - data["code_blocks"] = content_data["code_blocks"] - - if get_setting(self.config, "extract_links", True) and "links" in content_data: - data["links"] = content_data["links"] - - if get_setting(self.config, "extract_images", True) and "images" in content_data: - data["images"] = content_data["images"] - - def _add_document_metadata( - self, data: dict[str, Any], content_data: dict[str, Any], docname: str, title: str - ) -> None: - """Add document type and section metadata.""" - if get_setting(self.config, "include_doc_type", True): - discovery = DocumentDiscovery(self.app, self.json_builder) - data["doc_type"] = discovery.detect_document_type(docname, title, content_data.get("content", "")) - - if get_setting(self.config, "include_section_path", True): - discovery = DocumentDiscovery(self.app, self.json_builder) - data["section_path"] = discovery.get_section_path(docname) diff --git a/docs/_ext/json_output/core/json_writer.py b/docs/_ext/json_output/core/json_writer.py deleted file mode 100644 index 14eea68d1..000000000 --- a/docs/_ext/json_output/core/json_writer.py +++ /dev/null @@ -1,103 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""JSON file writing and output operations.""" - -import json -from pathlib import Path -from typing import Any - -from sphinx.application import Sphinx -from sphinx.util import logging - -from ..utils import get_setting - -logger = logging.getLogger(__name__) - - -class JSONWriter: - """Handles JSON file writing operations.""" - - def __init__(self, app: Sphinx): - self.app = app - self.config = app.config - - def write_json_file(self, docname: str, data: dict[str, Any]) -> None: - """Write JSON data to file.""" - try: - outdir = Path(self.app.outdir) - - if docname == "index": - json_path = outdir / "index.json" - elif docname.endswith("/index"): - json_path = outdir / docname[:-6] / "index.json" - else: - json_path = outdir / f"{docname}.json" - - json_path.parent.mkdir(parents=True, exist_ok=True) - - # For main index.json, output as array of page objects - if docname == "index" and "_documents_array" in data: - self._write_array_index(json_path, data) - # Handle separate content files option - elif get_setting(self.config, "separate_content", False) and "content" in data: - self._write_separate_content(json_path, data) - else: - self._write_single_file(json_path, data) - - logger.debug(f"Generated JSON: {json_path}") - - except Exception: - logger.exception(f"Failed to write JSON for {docname}") - - def _write_array_index(self, json_path: Path, data: dict[str, Any]) -> None: - """Write main index.json as an array of page objects for search engines.""" - # Extract the documents array and write as root-level array - documents = data.get("_documents_array", []) - self._write_json_data(json_path, documents) - logger.info(f"Generated search index array with {len(documents)} documents") - - def _write_separate_content(self, json_path: Path, data: dict[str, Any]) -> None: - """Write content to separate file when separate_content is enabled.""" - # Write content to separate file - content_path = json_path.with_suffix(".content.json") - content_data = { - "id": data["id"], - "content": data["content"], - "format": data.get("format", "text"), - "content_length": data.get("content_length", 0), - "word_count": data.get("word_count", 0), - } - - self._write_json_data(content_path, content_data) - - # Remove content from main data and add reference - main_data = data.copy() - del main_data["content"] - main_data["content_file"] = str(content_path.name) - - self._write_json_data(json_path, main_data) - - def _write_single_file(self, json_path: Path, data: dict[str, Any]) -> None: - """Write all data to a single JSON file.""" - self._write_json_data(json_path, data) - - def _write_json_data(self, file_path: Path, data: dict[str, Any]) -> None: - """Write JSON data to file with appropriate formatting.""" - with open(file_path, "w", encoding="utf-8") as f: - if get_setting(self.config, "minify_json", False): - json.dump(data, f, ensure_ascii=False, separators=(",", ":")) - else: - json.dump(data, f, ensure_ascii=False, indent=2) diff --git a/docs/_ext/json_output/processing/__init__.py b/docs/_ext/json_output/processing/__init__.py deleted file mode 100644 index f00304057..000000000 --- a/docs/_ext/json_output/processing/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Processing pipeline and orchestration components.""" - -from .cache import JSONOutputCache -from .processor import on_build_finished, process_document, process_documents_parallel, process_documents_sequential - -__all__ = [ - "JSONOutputCache", - "on_build_finished", - "process_document", - "process_documents_parallel", - "process_documents_sequential", -] diff --git a/docs/_ext/json_output/processing/cache.py b/docs/_ext/json_output/processing/cache.py deleted file mode 100644 index bc397dcf1..000000000 --- a/docs/_ext/json_output/processing/cache.py +++ /dev/null @@ -1,109 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Caching and incremental build support for JSON output extension.""" - -from collections.abc import Callable -from pathlib import Path -from threading import Lock -from typing import Any, ClassVar - -from sphinx.util import logging - -logger = logging.getLogger(__name__) - - -class JSONOutputCache: - """Manages caching and incremental builds for JSON output.""" - - # Class-level shared caches with thread safety - _shared_cache_lock = Lock() - _shared_metadata_cache: ClassVar[dict[str, Any]] = {} - _shared_frontmatter_cache: ClassVar[dict[str, Any]] = {} - _shared_content_cache: ClassVar[dict[str, Any]] = {} - _file_timestamps: ClassVar[dict[str, float]] = {} # Track file modification times - - def __init__(self): - """Initialize cache instance with shared caches.""" - with self._shared_cache_lock: - self._metadata_cache = self._shared_metadata_cache - self._frontmatter_cache = self._shared_frontmatter_cache - self._content_cache = self._shared_content_cache - self._timestamps = self._file_timestamps - - def get_metadata_cache(self) -> dict[str, Any]: - """Get the metadata cache.""" - return self._metadata_cache - - def get_frontmatter_cache(self) -> dict[str, Any]: - """Get the frontmatter cache.""" - return self._frontmatter_cache - - def get_content_cache(self) -> dict[str, Any]: - """Get the content cache.""" - return self._content_cache - - def needs_update(self, docname: str, source_path: Path, incremental_enabled: bool = False) -> bool: - """Check if document needs to be updated based on modification time.""" - if not incremental_enabled: - return True # Process all files if incremental build is disabled - - try: - if not source_path or not source_path.exists(): - return True - - current_mtime = source_path.stat().st_mtime - - # Check if we have a recorded timestamp - if docname in self._timestamps: - return current_mtime > self._timestamps[docname] - else: - # First time processing this file - self._timestamps[docname] = current_mtime - return True - - except Exception as e: # noqa: BLE001 - logger.debug(f"Error checking modification time for {docname}: {e}") - return True # Process if we can't determine modification time - - def mark_updated(self, docname: str, source_path: Path) -> None: - """Mark document as processed with current timestamp.""" - try: - if source_path and source_path.exists(): - self._timestamps[docname] = source_path.stat().st_mtime - except Exception: # noqa: BLE001 - logger.debug(f"Could not update timestamp for {docname}") - - def clear_caches(self) -> None: - """Clear all caches (useful for testing or memory cleanup).""" - with self._shared_cache_lock: - self._metadata_cache.clear() - self._frontmatter_cache.clear() - self._content_cache.clear() - self._timestamps.clear() - - def get_cache_stats(self) -> dict[str, int]: - """Get cache statistics for debugging.""" - return { - "metadata_cache_size": len(self._metadata_cache), - "frontmatter_cache_size": len(self._frontmatter_cache), - "content_cache_size": len(self._content_cache), - "timestamps_size": len(self._timestamps), - } - - def with_cache_lock(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any: # noqa: ANN401 - """Execute function with cache lock held.""" - with self._shared_cache_lock: - return func(*args, **kwargs) diff --git a/docs/_ext/json_output/processing/processor.py b/docs/_ext/json_output/processing/processor.py deleted file mode 100644 index 357fe83ff..000000000 --- a/docs/_ext/json_output/processing/processor.py +++ /dev/null @@ -1,214 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Document processing and build orchestration for JSON output extension.""" - -import multiprocessing -from collections.abc import Callable -from concurrent.futures import ThreadPoolExecutor - -from sphinx.application import Sphinx -from sphinx.config import Config -from sphinx.util import logging - -from ..core.builder import JSONOutputBuilder -from ..utils import get_setting, validate_content_gating_integration - -logger = logging.getLogger(__name__) - - -def on_build_finished(app: Sphinx, exception: Exception) -> None: - """Generate JSON files after HTML build is complete.""" - if exception is not None: - return - - verbose = get_setting(app.config, "verbose", False) - log_func = logger.info if verbose else logger.debug - log_func("Generating JSON output files...") - - # Setup and validation - json_builder = _setup_json_builder(app) - if not json_builder: - return - - # Get and filter documents - all_docs = _filter_documents(app, json_builder, log_func) - - # Process documents - generated_count, failed_count = _process_documents(app, json_builder, all_docs, log_func) - - # Final logging - _log_results(log_func, generated_count, failed_count) - - -def _setup_json_builder(app: Sphinx) -> JSONOutputBuilder | None: - """Setup and validate JSON builder.""" - validate_content_gating_integration(app) - - try: - return JSONOutputBuilder(app) - except Exception: - logger.exception("Failed to initialize JSONOutputBuilder") - return None - - -def _filter_documents(app: Sphinx, json_builder: JSONOutputBuilder, log_func: Callable[[str], None]) -> list[str]: - """Filter documents based on gating, incremental build, and size limits.""" - all_docs, gated_docs = _get_initial_documents(app, json_builder) - - if gated_docs: - log_func(f"Content gating: excluding {len(gated_docs)} documents from JSON generation") - verbose = get_setting(app.config, "verbose", False) - if verbose and gated_docs: - logger.debug(f"Gated documents: {', '.join(sorted(gated_docs))}") - - all_docs = _apply_incremental_filtering(app, json_builder, all_docs, log_func) - return _apply_size_filtering(app, all_docs, log_func) - - -def _get_initial_documents(app: Sphinx, json_builder: JSONOutputBuilder) -> tuple[list[str], list[str]]: - """Get initial document lists, separating processable from gated documents.""" - all_docs = [] - gated_docs = [] - - for docname in app.env.all_docs: - if json_builder.should_generate_json(docname): - all_docs.append(docname) - else: - gated_docs.append(docname) - - return all_docs, gated_docs - - -def _apply_incremental_filtering( - app: Sphinx, json_builder: JSONOutputBuilder, all_docs: list[str], log_func: Callable[[str], None] -) -> list[str]: - """Apply incremental build filtering if enabled.""" - if not get_setting(app.config, "incremental_build", False): - return all_docs - - incremental_docs = [docname for docname in all_docs if json_builder.needs_update(docname)] - skipped_count = len(all_docs) - len(incremental_docs) - if skipped_count > 0: - log_func(f"Incremental build: skipping {skipped_count} unchanged files") - return incremental_docs - - -def _apply_size_filtering(app: Sphinx, all_docs: list[str], log_func: Callable[[str], None]) -> list[str]: - """Apply file size filtering if enabled.""" - skip_large_files = get_setting(app.config, "skip_large_files", 0) - if skip_large_files <= 0: - return all_docs - - filtered_docs = [] - for docname in all_docs: - try: - source_path = app.env.doc2path(docname) - if source_path and source_path.stat().st_size <= skip_large_files: - filtered_docs.append(docname) - else: - log_func(f"Skipping large file: {docname} ({source_path.stat().st_size} bytes)") - except Exception: # noqa: BLE001, PERF203 - filtered_docs.append(docname) # Include if we can't check size - return filtered_docs - - -def _process_documents( - app: Sphinx, json_builder: JSONOutputBuilder, all_docs: list[str], log_func: Callable[[str], None] -) -> tuple[int, int]: - """Process documents either in parallel or sequentially.""" - if get_setting(app.config, "parallel", False): - return process_documents_parallel(json_builder, all_docs, app.config, log_func) - else: - return process_documents_sequential(json_builder, all_docs) - - -def _log_results(log_func: Callable[[str], None], generated_count: int, failed_count: int) -> None: - """Log final processing results.""" - log_func(f"Generated {generated_count} JSON files") - if failed_count > 0: - logger.warning(f"Failed to generate {failed_count} JSON files") - - -def process_documents_parallel( - json_builder: JSONOutputBuilder, all_docs: list[str], config: Config, log_func: Callable[[str], None] -) -> tuple[int, int]: - """Process documents in parallel batches.""" - parallel_workers = get_setting(config, "parallel_workers", "auto") - if parallel_workers == "auto": - cpu_count = multiprocessing.cpu_count() or 1 - max_workers = min(cpu_count, 8) # Limit to 8 threads max - else: - max_workers = min(int(parallel_workers), 16) # Cap at 16 for safety - - batch_size = get_setting(config, "batch_size", 50) - - generated_count = 0 - failed_count = 0 - - # Process in batches to control memory usage - for i in range(0, len(all_docs), batch_size): - batch_docs = all_docs[i : i + batch_size] - log_func( - f"Processing batch {i // batch_size + 1}/{(len(all_docs) - 1) // batch_size + 1} ({len(batch_docs)} docs)" - ) - - with ThreadPoolExecutor(max_workers=max_workers) as executor: - futures = {} - for docname in batch_docs: - future = executor.submit(process_document, json_builder, docname) - futures[future] = docname - - for future, docname in futures.items(): - try: - if future.result(): - generated_count += 1 - else: - failed_count += 1 - except Exception: # noqa: PERF203 - logger.exception(f"Error generating JSON for {docname}") - failed_count += 1 - - return generated_count, failed_count - - -def process_documents_sequential(json_builder: JSONOutputBuilder, all_docs: list[str]) -> tuple[int, int]: - """Process documents sequentially.""" - generated_count = 0 - failed_count = 0 - - for docname in all_docs: - try: - json_data = json_builder.build_json_data(docname) - json_builder.write_json_file(docname, json_data) - generated_count += 1 - except Exception: # noqa: PERF203 - logger.exception(f"Error generating JSON for {docname}") - failed_count += 1 - - return generated_count, failed_count - - -def process_document(json_builder: JSONOutputBuilder, docname: str) -> bool: - """Process a single document for parallel execution.""" - try: - json_data = json_builder.build_json_data(docname) - json_builder.write_json_file(docname, json_data) - json_builder.mark_updated(docname) # Mark as processed for incremental builds - except Exception: - logger.exception(f"Error generating JSON for {docname}") - return False - else: - return True diff --git a/docs/_ext/json_output/utils.py b/docs/_ext/json_output/utils.py deleted file mode 100644 index 43fbc044b..000000000 --- a/docs/_ext/json_output/utils.py +++ /dev/null @@ -1,137 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Utility functions for JSON output.""" - -import fnmatch -from typing import Any - -from sphinx.application import Sphinx -from sphinx.config import Config -from sphinx.util import logging - -logger = logging.getLogger(__name__) - - -def validate_content_gating_integration(app: Sphinx) -> None: - """Validate that content gating integration is working properly.""" - # Check if content_gating extension is loaded - if "content_gating" in app.extensions: - logger.info("Content gating extension detected - JSON output will respect content gating rules") - else: - logger.debug("Content gating extension not detected - JSON output will process all documents") - - # Log current exclude patterns for debugging - exclude_patterns = getattr(app.config, "exclude_patterns", []) - if exclude_patterns: - logger.debug(f"Current exclude patterns: {exclude_patterns}") - - # Check current build tags for debugging - if hasattr(app, "tags"): - try: - current_tags = set(app.tags) - if current_tags: - logger.info(f"Active build tags: {current_tags}") - else: - logger.info("No build tags active") - except (TypeError, AttributeError): - logger.debug("Could not determine active build tags") - - -def get_setting(config: Config, key: str, default: Any = None) -> Any: # noqa: ANN401 - """Get a setting from json_output_settings with fallback to old config names.""" - settings = getattr(config, "json_output_settings", {}) - - # Try new settings format first - if key in settings: - return settings[key] - - # Fallback to old config names for backward compatibility - old_config_map = { - "enabled": "json_output_enabled", - "exclude_patterns": "json_output_exclude_patterns", - "verbose": "json_output_verbose", - "parallel": "json_output_parallel", - "include_children": "json_output_include_children", - "include_child_content": "json_output_include_child_content", - "main_index_mode": "json_output_main_index_mode", - "max_main_index_docs": "json_output_max_main_index_docs", - } - - old_key = old_config_map.get(key) - if old_key and hasattr(config, old_key): - return getattr(config, old_key) - - return default - - -def is_content_gated(config: Config, docname: str) -> bool: - """ - Check if a document is content gated by checking Sphinx's exclude_patterns. - This works with the content_gating extension that adds restricted documents - to exclude_patterns during config-inited event. - """ - sphinx_exclude_patterns = getattr(config, "exclude_patterns", []) - if not sphinx_exclude_patterns: - return False - - # Convert docname to potential file paths that might be in exclude_patterns - possible_paths = [docname + ".md", docname + ".rst", docname] - - for possible_path in possible_paths: - # Check if this path matches any exclude pattern using fnmatch (supports glob patterns) - for pattern in sphinx_exclude_patterns: - if isinstance(pattern, str) and fnmatch.fnmatch(possible_path, pattern): - logger.debug(f"Document {docname} is content gated (matches pattern: {pattern})") - return True - - return False - - -def should_generate_json(config: Config, docname: str) -> bool: - """Check if JSON should be generated for this document.""" - if not get_setting(config, "enabled", True): - return False - - if not docname or not isinstance(docname, str): - logger.warning(f"Invalid docname for JSON generation: {docname}") - return False - - # CRITICAL: Check content gating first - if document is content gated, don't generate JSON - if is_content_gated(config, docname): - logger.info(f"Excluding {docname} from JSON generation due to content gating") - return False - - # Check JSON output extension's own exclude patterns - for pattern in get_setting(config, "exclude_patterns", []): - if isinstance(pattern, str) and docname.startswith(pattern): - return False - - return True - - -def get_document_url(app: Sphinx, docname: str) -> str: - """Get the URL for a document.""" - if not docname or not isinstance(docname, str): - logger.warning(f"Invalid docname for URL generation: {docname}") - return "invalid.html" - - try: - if hasattr(app.builder, "get_target_uri"): - return app.builder.get_target_uri(docname) - except Exception as e: # noqa: BLE001 - logger.warning(f"Failed to get target URI for {docname}: {e}") - - return docname + ".html" diff --git a/docs/_ext/policy_table.py b/docs/_ext/policy_table.py deleted file mode 100644 index 0838c9704..000000000 --- a/docs/_ext/policy_table.py +++ /dev/null @@ -1,235 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Sphinx extension that generates tables from a sandbox policy YAML file. - -Usage in MyST markdown:: - - ```{policy-table} path/to/sandbox-policy.yaml - ``` - -The directive reads the YAML relative to the repo root and emits: - 1. A "Filesystem, Landlock, and Process" table. - 2. One subsection per ``network_policies`` block with endpoint and binary tables. -""" - -from __future__ import annotations - -from pathlib import Path -from typing import Any - -import yaml -from docutils import nodes -from docutils.statemachine import StringList -from sphinx.application import Sphinx -from sphinx.util.docutils import SphinxDirective - - -def _tls_display(ep: dict[str, Any]) -> str: - tls = ep.get("tls") - return tls if tls else "\u2014" - - -def _access_display(ep: dict[str, Any]) -> str: - if "rules" in ep: - rules = ep["rules"] - parts = [] - for r in rules: - allow = r.get("allow", {}) - parts.append(f"``{allow.get('method', '*')} {allow.get('path', '/**')}``") - return ", ".join(parts) - access = ep.get("access") - if access: - return access - return "L4 passthrough" - - -def _binaries_line(binaries: list[dict[str, str]]) -> str: - paths = [f"``{b['path']}``" for b in binaries] - return ", ".join(paths) - - -BLOCK_INFO: dict[str, dict[str, str]] = { - "claude_code": { - "title": "Anthropic API and Telemetry", - "description": ( - "Allows Claude Code to reach its API, feature-flagging " - "(Statsig), error reporting (Sentry), release notes, and " - "the Claude platform dashboard." - ), - }, - "github_ssh_over_https": { - "title": "Git Clone and Fetch", - "description": ( - "Allows ``git clone``, ``git fetch``, and ``git pull`` over " - "HTTPS via Git Smart HTTP. Push (``git-receive-pack``) is " - "disabled by default." - ), - }, - "nvidia_inference": { - "title": "NVIDIA API Catalog", - "description": ( - "Allows outbound calls to the NVIDIA hosted inference API. " - "Used by agents that route LLM requests through " - "``integrate.api.nvidia.com``." - ), - }, - "github_rest_api": { - "title": "GitHub API (Read-Only)", - "description": ( - "Grants read-only access to the GitHub REST API. Enables " - "issue reads, PR listing, and repository metadata lookups " - "without allowing mutations." - ), - }, - "pypi": { - "title": "Python Package Installation", - "description": ( - "Allows ``pip install`` and ``uv pip install`` to reach PyPI, " - "python-build-standalone releases on GitHub, and " - "``downloads.python.org``." - ), - }, - "vscode": { - "title": "VS Code Remote and Marketplace", - "description": ( - "Allows VS Code Server, Remote Containers, and extension " - "marketplace traffic so remote development sessions can " - "download updates and extensions." - ), - }, - "gitlab": { - "title": "GitLab", - "description": ( - "Allows the ``glab`` CLI to reach ``gitlab.com`` for " - "repository and merge-request operations." - ), - }, -} - - -def _block_title(key: str, name: str) -> str: - info = BLOCK_INFO.get(key) - return info["title"] if info else name - - -def _block_description(key: str) -> str | None: - info = BLOCK_INFO.get(key) - return info["description"] if info else None - - -class PolicyTableDirective(SphinxDirective): - """Render sandbox policy YAML as tables.""" - - required_arguments = 1 - has_content = False - - def run(self) -> list[nodes.Node]: - repo_root = Path(self.env.srcdir).parent - yaml_path = repo_root / self.arguments[0] - - self.env.note_dependency(str(yaml_path)) - - if not yaml_path.exists(): - msg = self.state_machine.reporter.warning( - f"Policy YAML not found: {yaml_path}", - line=self.lineno, - ) - return [msg] - - policy = yaml.safe_load(yaml_path.read_text()) - - lines: list[str] = [] - - fs = policy.get("filesystem_policy", {}) - landlock = policy.get("landlock", {}) - proc = policy.get("process", {}) - - lines.append("(default-policy-fs-landlock-process)=") - lines.append("

Filesystem, Landlock, and Process

") - lines.append("") - lines.append("| Section | Setting | Value |") - lines.append("|---|---|---|") - - ro = fs.get("read_only", []) - rw = fs.get("read_write", []) - workdir = fs.get("include_workdir", False) - lines.append( - f"| **Filesystem** | Read-only | {', '.join(f'``{p}``' for p in ro)} |" - ) - lines.append(f"| | Read-write | {', '.join(f'``{p}``' for p in rw)} |") - lines.append(f"| | Workdir included | {'Yes' if workdir else 'No'} |") - - compat = landlock.get("compatibility", "best_effort") - lines.append( - f"| **Landlock** | Compatibility | ``{compat}`` " - f"(uses the highest ABI the host kernel supports) |" - ) - - user = proc.get("run_as_user", "") - group = proc.get("run_as_group", "") - lines.append(f"| **Process** | User / Group | ``{user}`` / ``{group}`` |") - lines.append("") - - net = policy.get("network_policies", {}) - if net: - lines.append("(default-policy-network-policies)=") - lines.append("

Network Policy Blocks

") - lines.append("") - lines.append( - "Each block pairs a set of endpoints (host and port) with " - "a set of binaries (executable paths inside the sandbox). " - "The proxy identifies the calling binary by resolving the " - "socket to a PID through ``/proc/net/tcp`` and reading " - "``/proc/{pid}/exe``. A connection is allowed only when both " - "the destination and the calling binary match an entry in the " - "same block. All other outbound traffic is denied." - ) - lines.append("") - - for key, block in net.items(): - name = block.get("name", key) - endpoints = block.get("endpoints", []) - binaries = block.get("binaries", []) - - lines.append(f"

{_block_title(key, name)}

") - lines.append("") - desc = _block_description(key) - if desc: - lines.append(desc) - lines.append("") - - has_rules = any("rules" in ep for ep in endpoints) - if has_rules: - lines.append("| Endpoint | Port | TLS | Rules |") - else: - lines.append("| Endpoint | Port | TLS | Access |") - lines.append("|---|---|---|---|") - - for ep in endpoints: - host = ep.get("host", "") - port = ep.get("port", "") - tls = _tls_display(ep) - access = _access_display(ep) - lines.append(f"| ``{host}`` | {port} | {tls} | {access} |") - - lines.append("") - lines.append( - f"Only the following binaries can use these endpoints: " - f"{_binaries_line(binaries)}." - ) - lines.append("") - - rst = StringList(lines, source=str(yaml_path)) - container = nodes.container() - self.state.nested_parse(rst, self.content_offset, container) - return container.children - - -def setup(app: Sphinx) -> dict[str, Any]: - app.add_directive("policy-table", PolicyTableDirective) - return { - "version": "0.1", - "parallel_read_safe": True, - "parallel_write_safe": True, - } diff --git a/docs/_ext/search_assets/__init__.py b/docs/_ext/search_assets/__init__.py deleted file mode 100644 index 032e149a7..000000000 --- a/docs/_ext/search_assets/__init__.py +++ /dev/null @@ -1,202 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Enhanced Search Extension for Sphinx -Provides enhanced search page functionality without interfering with default search -""" - -import os -import re -import shutil -from typing import Any - -from sphinx.application import Sphinx -from sphinx.config import Config -from sphinx.util import logging - -logger = logging.getLogger(__name__) - - -def bundle_javascript_modules(extension_dir: str, output_path: str, minify: bool = False) -> None: - """Bundle all JavaScript modules into a single file.""" - - # Define the module loading order (dependencies first) - module_files = [ - ("modules", "Utils.js"), - ("modules", "DocumentLoader.js"), - ("modules", "SearchEngine.js"), - ("modules", "SearchInterface.js"), - ("modules", "ResultRenderer.js"), - ("modules", "EventHandler.js"), - ("modules", "SearchPageManager.js"), - ("", "main.js"), # Main file in root - ] - - bundled_content = [] - bundled_content.append("// Enhanced Search Bundle - Generated automatically") - bundled_content.append( - "// Contains: Utils, DocumentLoader, SearchEngine, SearchInterface, ResultRenderer, EventHandler, SearchPageManager, main" - ) - bundled_content.append("") - - for subdir, filename in module_files: - if subdir: - module_path = os.path.join(extension_dir, subdir, filename) - else: - module_path = os.path.join(extension_dir, filename) - - if os.path.exists(module_path): - with open(module_path, encoding="utf-8") as f: - content = f.read() - - # Remove module loading code since everything is bundled - content = content.replace("await this.loadModules();", "// Modules bundled - no loading needed") - content = content.replace( - "await this.loadModuleWithFallback(name)", "// Modules bundled - no loading needed" - ) - - # Simple minification if requested - if minify: - # Remove extra whitespace and comments (basic minification) - # Remove single-line comments but preserve URLs - content = re.sub(r"^\s*//.*$", "", content, flags=re.MULTILINE) - # Remove multi-line comments - content = re.sub(r"/\*.*?\*/", "", content, flags=re.DOTALL) - # Remove extra whitespace - content = re.sub(r"\n\s*\n", "\n", content) - content = re.sub(r"^\s+", "", content, flags=re.MULTILINE) - - bundled_content.append(f"// === {filename} ===") - bundled_content.append(content) - bundled_content.append("") - - logger.info(f"Bundled: {filename}") - else: - logger.warning(f"Module not found for bundling: {module_path}") - - # Write the bundled file - with open(output_path, "w", encoding="utf-8") as f: - f.write("\n".join(bundled_content)) - - file_size = os.path.getsize(output_path) - size_kb = file_size / 1024 - logger.info(f"Enhanced Search JavaScript bundle created: {output_path} ({size_kb:.1f}KB)") - - -def add_template_path(_app: Sphinx, config: Config) -> None: - """Add template path during config initialization.""" - extension_dir = os.path.dirname(os.path.abspath(__file__)) - templates_path = os.path.join(extension_dir, "templates") - - if os.path.exists(templates_path): - # Ensure templates_path is a list - if not isinstance(config.templates_path, list): - config.templates_path = list(config.templates_path) if config.templates_path else [] - - # Add our template path if not already present - if templates_path not in config.templates_path: - config.templates_path.append(templates_path) - logger.info(f"Enhanced search templates added: {templates_path}") - - -def copy_assets(app: Sphinx, exc: Exception | None) -> None: - """Copy assets to _static after build.""" - if exc is not None: # Only run if build succeeded - return - - extension_dir = os.path.dirname(os.path.abspath(__file__)) - static_path = os.path.join(app.outdir, "_static") - os.makedirs(static_path, exist_ok=True) - - # Copy CSS file - css_file = os.path.join(extension_dir, "enhanced-search.css") - if os.path.exists(css_file): - shutil.copy2(css_file, os.path.join(static_path, "enhanced-search.css")) - logger.info("Enhanced search CSS copied") - - # Copy main JavaScript file - main_js = os.path.join(extension_dir, "main.js") - if os.path.exists(main_js): - shutil.copy2(main_js, os.path.join(static_path, "main.js")) - logger.info("Enhanced search main.js copied") - - # Copy module files - modules_dir = os.path.join(extension_dir, "modules") - if os.path.exists(modules_dir): - modules_static_dir = os.path.join(static_path, "modules") - os.makedirs(modules_static_dir, exist_ok=True) - for module_file in os.listdir(modules_dir): - if module_file.endswith(".js"): - shutil.copy2(os.path.join(modules_dir, module_file), os.path.join(modules_static_dir, module_file)) - logger.info("Enhanced search modules copied") - - -def copy_assets_early(app: Sphinx, _docname: str, _source: list[str]) -> None: - """Copy bundled assets to _static early in the build process.""" - # Only copy once - use a flag to prevent multiple copies - if hasattr(app, "_search_assets_copied"): - return - - extension_dir = os.path.dirname(os.path.abspath(__file__)) - static_path = os.path.join(app.outdir, "_static") - os.makedirs(static_path, exist_ok=True) - - # Copy CSS file - css_file = os.path.join(extension_dir, "enhanced-search.css") - if os.path.exists(css_file): - shutil.copy2(css_file, os.path.join(static_path, "enhanced-search.css")) - logger.info("Enhanced search CSS copied") - - # Create bundled JavaScript file instead of copying individual modules - bundle_path = os.path.join(static_path, "search-assets.bundle.js") - bundle_javascript_modules(extension_dir, bundle_path) - - # Mark as copied - app._search_assets_copied = True - - -def setup(app: Sphinx) -> dict[str, Any]: - """Setup the enhanced search extension.""" - - # Get the directory where this extension is located - extension_dir = os.path.dirname(os.path.abspath(__file__)) - - # Connect to config-inited event to add template path - app.connect("config-inited", add_template_path) - - # Copy assets early in the build process so JS modules are available - app.connect("source-read", copy_assets_early) - - # Add CSS file - css_file = os.path.join(extension_dir, "enhanced-search.css") - if os.path.exists(css_file): - app.add_css_file("enhanced-search.css") - logger.info("Enhanced search CSS loaded") - else: - logger.warning(f"Enhanced search CSS not found at {css_file}") - - # Add the bundled JavaScript file (contains all modules) - app.add_js_file("search-assets.bundle.js") - logger.info("Enhanced search bundled JS will be loaded") - - # Connect to build events (backup) - app.connect("build-finished", copy_assets) - - return { - "version": "2.0.0", - "parallel_read_safe": True, - "parallel_write_safe": True, - } diff --git a/docs/_ext/search_assets/enhanced-search.css b/docs/_ext/search_assets/enhanced-search.css deleted file mode 100644 index 6ae98c88c..000000000 --- a/docs/_ext/search_assets/enhanced-search.css +++ /dev/null @@ -1,1370 +0,0 @@ -/** - * Enhanced Search Styles - * Aligned with NVIDIA Sphinx theme - full light/dark mode support - * Uses theme variables exclusively - no hardcoded colors - */ - -/* CSS Variables for theming */ -:root { - --search-primary-color: var(--nv-color-green, #76b900); - --search-background: var(--pst-color-background, #ffffff); - --search-surface: var(--pst-color-surface, #f8f9fa); - --search-text-primary: var(--pst-color-text-base, #333333); - --search-text-secondary: var(--pst-color-text-muted, #6c757d); - --search-border: var(--pst-color-border, #e1e4e8); - --search-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - --search-font-family: var(--pst-font-family-base, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif); -} - -/* ===== SEARCH PAGE STYLES ===== */ - -/* Unified Search Controls Container */ -.search-controls-container { - background: linear-gradient(to bottom, var(--pst-color-background), var(--pst-color-surface)); - border: 1px solid var(--pst-color-on-surface); - border-radius: 1rem; - padding: 1.5rem; - box-shadow: - 0 4px 6px -1px rgba(0, 0, 0, 0.05), - 0 2px 4px -1px rgba(0, 0, 0, 0.03), - inset 0 1px 0 rgba(255, 255, 255, 0.1); -} - -/* Search Filters */ -.search-filters { - margin-bottom: 1.25rem; -} - -/* Filter Header */ -.filter-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 1rem; - padding-bottom: 0.75rem; - border-bottom: 1px solid var(--pst-color-on-surface); -} - -.filter-header-left { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.filter-header-icon { - color: var(--nv-color-green); - font-size: 0.875rem; -} - -.filter-header-title { - font-size: 0.8125rem; - font-weight: 600; - color: var(--pst-color-text-base); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.active-filter-count { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 1.25rem; - height: 1.25rem; - padding: 0 0.375rem; - font-size: 0.6875rem; - font-weight: 700; - color: white; - background: var(--nv-color-green); - border-radius: 1rem; -} - -.filter-clear-btn { - display: inline-flex; - align-items: center; - gap: 0.375rem; - padding: 0.375rem 0.75rem; - font-size: 0.75rem; - font-weight: 500; - color: var(--pst-color-text-muted); - background: transparent; - border: 1px solid var(--pst-color-on-surface); - border-radius: 0.375rem; - cursor: pointer; - transition: all 0.2s ease; -} - -.filter-clear-btn:hover { - color: var(--pst-color-text-base); - background: var(--pst-color-surface); - border-color: var(--pst-color-text-muted); -} - -.filter-clear-btn.hidden { - opacity: 0; - pointer-events: none; -} - -/* Filter Grid */ -.filter-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); - gap: 1rem; -} - -.filter-group { - display: flex; - flex-direction: column; - gap: 0.375rem; -} - -.filter-label { - display: flex; - align-items: center; - gap: 0.375rem; - font-size: 0.6875rem; - font-weight: 600; - color: var(--pst-color-text-muted); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.filter-label i { - font-size: 0.625rem; - color: var(--pst-color-text-muted); - opacity: 0.7; -} - -/* Filter Select Wrapper */ -.filter-select-wrapper { - position: relative; - display: flex; - align-items: center; -} - -.filter-select-wrapper.has-value { - --select-border-color: var(--nv-color-green); - --select-bg-color: rgba(118, 185, 0, 0.05); -} - -.filter-select { - width: 100%; - padding: 0.5rem 2rem 0.5rem 0.75rem; - font-size: 0.8125rem; - font-family: var(--pst-font-family-base); - color: var(--pst-color-text-base); - background-color: var(--select-bg-color, var(--pst-color-background)); - border: 1px solid var(--select-border-color, var(--pst-color-on-surface)); - border-radius: 0.5rem; - outline: none; - appearance: none; - cursor: pointer; - transition: all 0.2s ease; - text-overflow: ellipsis; -} - -.filter-select:focus { - border-color: var(--nv-color-green); - box-shadow: 0 0 0 3px rgba(118, 185, 0, 0.15); -} - -.filter-select:hover:not(:focus) { - border-color: var(--pst-color-text-muted); - background-color: var(--pst-color-surface); -} - -.filter-select-arrow { - position: absolute; - right: 0.625rem; - font-size: 0.625rem; - color: var(--pst-color-text-muted); - pointer-events: none; - transition: transform 0.2s ease; -} - -.filter-select:focus+.filter-select-arrow { - color: var(--nv-color-green); -} - -.filter-select option { - background-color: var(--pst-color-background); - color: var(--pst-color-text-base); - padding: 0.5rem; -} - -/* Search Input Wrapper */ -.search-input-wrapper { - position: relative; - display: flex; - align-items: center; -} - -.search-input-icon { - position: absolute; - left: 1rem; - font-size: 1rem; - color: var(--pst-color-text-muted); - pointer-events: none; - transition: color 0.2s ease; - z-index: 1; -} - -.search-input-field { - width: 100%; - padding: 0.875rem 1rem 0.875rem 2.75rem; - font-size: 1rem; - font-family: var(--pst-font-family-base); - font-weight: 400; - line-height: 1.5; - color: var(--pst-color-text-base); - background-color: var(--pst-color-background); - border: 2px solid var(--pst-color-on-surface); - border-radius: 0.75rem; - outline: none; - transition: all 0.2s ease; -} - -.search-input-field:focus { - border-color: var(--nv-color-green); - box-shadow: 0 0 0 4px rgba(118, 185, 0, 0.12); -} - -.search-input-field:focus+.search-input-icon, -.search-input-wrapper:focus-within .search-input-icon { - color: var(--nv-color-green); -} - -.search-input-field::placeholder { - color: var(--pst-color-text-muted); - opacity: 0.8; -} - -/* Legacy filter-row support */ -.filter-row { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); - gap: 1rem; - align-items: end; -} - -.filter-actions { - display: flex; - align-items: center; - gap: 0.5rem; - justify-self: end; -} - -.btn { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - font-size: 0.875rem; - font-weight: 500; - font-family: var(--pst-font-family-base); - text-decoration: none; - border-radius: 0.25rem; - border: 1px solid transparent; - cursor: pointer; - transition: all 0.15s ease-in-out; -} - -.btn-sm { - padding: 0.375rem 0.75rem; - font-size: 0.8125rem; -} - -.btn-secondary { - color: var(--pst-color-text-base); - background-color: transparent; - border-color: var(--pst-color-on-surface); -} - -.btn-secondary:hover { - color: var(--pst-color-background); - background-color: var(--pst-color-text-base); - border-color: var(--pst-color-text-base); -} - -.btn-secondary:focus { - color: var(--pst-color-text-base); - background-color: transparent; - border-color: var(--nv-color-green); - box-shadow: 0 0 0 0.2rem rgba(118, 185, 0, 0.25); -} - -.btn-outline-secondary { - color: var(--pst-color-text-base); - background-color: transparent; - border-color: var(--pst-color-on-surface); -} - -.btn-outline-secondary:hover { - color: var(--pst-color-background); - background-color: var(--pst-color-text-base); - border-color: var(--pst-color-text-base); -} - -/* Responsive filters */ -@media (max-width: 900px) { - .filter-grid { - grid-template-columns: repeat(3, 1fr); - } -} - -@media (max-width: 768px) { - .search-controls-container { - padding: 1rem; - border-radius: 0.75rem; - } - - .search-filters { - margin-bottom: 1rem; - } - - .filter-header { - flex-wrap: wrap; - gap: 0.75rem; - } - - .filter-grid { - grid-template-columns: repeat(2, 1fr); - gap: 0.75rem; - } - - .filter-group { - min-width: auto; - } - - .filter-actions { - grid-column: 1; - justify-self: center; - margin-top: 0.75rem; - } - - .search-input-field { - padding: 0.75rem 1rem 0.75rem 2.5rem; - font-size: 1rem; - } -} - -@media (max-width: 480px) { - .filter-grid { - grid-template-columns: 1fr; - } - - .filter-header-left { - flex: 1; - } -} - -/* Legacy input ID selector - now handled by .search-input-field */ -#enhanced-search-page-input { - width: 100%; - padding: 0.875rem 1rem 0.875rem 2.75rem; - font-size: 1rem; - font-family: var(--pst-font-family-base); - font-weight: 400; - line-height: 1.5; - color: var(--pst-color-text-base); - background-color: var(--pst-color-background); - border: 2px solid var(--pst-color-on-surface); - border-radius: 0.75rem; - outline: none; - transition: all 0.2s ease; -} - -.search-input-unified { - margin-top: 0 !important; -} - -#enhanced-search-page-input:focus { - border-color: var(--nv-color-green); - box-shadow: 0 0 0 4px rgba(118, 185, 0, 0.12); -} - -#enhanced-search-page-input::placeholder { - color: var(--pst-color-text-muted); - opacity: 0.8; -} - -.loading { - display: inline-block; - margin-left: 0.5rem; - color: var(--pst-color-text-muted); -} - -.spinner { - display: inline-block; - width: 1rem; - height: 1rem; - border: 0.125rem solid var(--pst-color-text-muted); - border-radius: 50%; - border-top-color: var(--nv-color-green); - animation: spin 1s ease-in-out infinite; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -#search-results { - margin-top: 1.5rem; -} - -/* ===== SEARCH RESULTS STYLES ===== */ - -.search-results-header { - margin-bottom: 1.5rem; - padding-bottom: 1rem; - border-bottom: 1px solid var(--pst-color-on-surface); -} - -.search-results-header h3 { - color: var(--pst-color-heading); - font-family: var(--pst-font-family-heading); - font-weight: var(--pst-font-weight-heading); - font-size: var(--pst-font-size-h3); - margin: 0 0 0.5rem 0; -} - -.search-results-header p { - color: var(--pst-color-text-muted); - font-size: 0.875rem; - margin: 0; -} - -/* Search Result Cards */ -.search-result { - background-color: var(--pst-color-background); - border: 1px solid var(--pst-color-on-surface); - border-radius: 0.5rem; - padding: 1.5rem; - margin-bottom: 1.5rem; - transition: all 0.2s ease-in-out; - position: relative; - overflow: hidden; -} - -.search-result::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: linear-gradient(90deg, var(--nv-color-green), var(--nv-color-green-2)); - transform: scaleX(0); - transform-origin: left; - transition: transform 0.2s ease-in-out; -} - -.search-result:hover { - border-color: var(--nv-color-green); - box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1); - transform: translateY(-0.125rem); -} - -.search-result:hover::before { - transform: scaleX(1); -} - -/* Result Header */ -.result-header { - display: flex; - align-items: flex-start; - gap: 1rem; - margin-bottom: 1rem; -} - -.section-icon { - flex-shrink: 0; - width: 3rem; - height: 3rem; - border-radius: 0.5rem; - display: flex; - align-items: center; - justify-content: center; - font-size: 1.25rem; - font-weight: 700; - color: var(--pst-color-background); - background: var(--nv-color-green); - border: 1px solid var(--pst-color-on-surface); -} - -.result-info { - flex-grow: 1; - min-width: 0; -} - -.result-title { - margin: 0 0 0.5rem 0; - font-family: var(--pst-font-family-heading); - font-weight: var(--pst-font-weight-heading); - font-size: var(--pst-font-size-h4); - line-height: 1.25; -} - -.result-title a { - color: var(--pst-color-heading); - text-decoration: none; - transition: color 0.15s ease-in-out; -} - -.result-title a:hover { - color: var(--nv-color-green); - text-decoration: underline; - text-decoration-color: var(--nv-color-green); - text-decoration-thickness: max(3px, 0.1875rem, 0.12em); -} - -/* Breadcrumb */ -.result-breadcrumb { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.875rem; - color: var(--pst-color-text-muted); - margin-bottom: 0.5rem; - font-family: var(--pst-font-family-base); -} - -.result-breadcrumb .breadcrumb-separator { - color: var(--pst-color-text-muted); - font-weight: 400; -} - -/* Meta Information */ -.result-meta { - display: flex; - align-items: center; - gap: 1rem; - flex-wrap: wrap; -} - -.section-badge { - display: inline-flex; - align-items: center; - gap: 0.25rem; - padding: 0.25rem 0.5rem; - background-color: var(--pst-color-background); - border: 1px solid var(--pst-color-on-surface); - border-radius: 1rem; - font-size: 0.75rem; - font-weight: 500; - color: var(--pst-color-text-base); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.relevance-score { - font-size: 0.75rem; - color: var(--pst-color-text-muted); - font-weight: 500; - font-family: var(--pst-font-family-monospace); -} - -/* Result Content */ -.result-content { - color: var(--pst-color-text-base); - font-family: var(--pst-font-family-base); - line-height: 1.6; - margin-bottom: 1rem; -} - -.result-content p { - margin: 0 0 0.75rem 0; -} - -.result-content p:last-child { - margin-bottom: 0; -} - -.result-summary { - color: var(--pst-color-text-base); - font-size: 0.9rem; - line-height: 1.5; - margin-bottom: 1rem; -} - -/* Matching Sections */ -.matching-sections { - margin-top: 1rem; - padding-top: 1rem; - border-top: 1px solid var(--pst-color-on-surface); -} - -.matching-sections h4, -.matching-sections h5 { - color: var(--pst-color-heading); - font-family: var(--pst-font-family-heading); - font-weight: 500; - font-size: 0.875rem; - text-transform: uppercase; - letter-spacing: 0.05em; - margin: 0 0 0.75rem 0; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.section-links { - background-color: var(--pst-color-background); - border: 1px solid var(--pst-color-on-surface); - border-radius: 0.5rem; - padding: 0.75rem; -} - -.section-link { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.5rem 0.75rem; - border-radius: 0.25rem; - font-size: 0.875rem; - color: var(--pst-color-text-base); - text-decoration: none; - transition: all 0.15s ease-in-out; - font-family: var(--pst-font-family-base); - margin-bottom: 0.25rem; -} - -.section-link:last-child { - margin-bottom: 0; -} - -.section-link:hover { - background-color: var(--nv-color-green); - color: var(--pst-color-background); - text-decoration: none; - transform: translateY(-0.0625rem); - box-shadow: 0 0.25rem 0.5rem rgba(118, 185, 0, 0.25); -} - -.section-link .section-icon { - width: 1.5rem; - height: 1.5rem; - font-size: 0.875rem; - background: var(--pst-color-surface); - color: var(--pst-color-primary); -} - -.section-link:hover .section-icon { - background: var(--pst-color-background); - color: var(--nv-color-green); -} - -/* Enhanced Result Features */ -.result-tag, -.result-category { - display: inline-flex; - align-items: center; - padding: 0.25rem 0.5rem; - font-size: 0.75rem; - font-weight: 500; - border-radius: 0.25rem; - text-decoration: none; - margin-right: 0.25rem; - margin-bottom: 0.25rem; -} - -.result-tag { - background-color: var(--pst-color-surface); - color: var(--pst-color-text-base); - border: 1px solid var(--pst-color-on-surface); - font-size: 0.75rem; - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; - display: inline-block; - margin-right: 0.5rem; - margin-bottom: 0.25rem; -} - -.result-category { - background-color: rgba(118, 185, 0, 0.1); - color: var(--nv-color-green); - border: 1px solid rgba(118, 185, 0, 0.2); -} - -.multiple-matches-indicator { - display: inline-flex; - align-items: center; - padding: 0.25rem 0.5rem; - font-size: 0.75rem; - font-weight: 500; - color: var(--nv-color-green); - background-color: rgba(118, 185, 0, 0.1); - border-radius: 0.25rem; - border: 1px solid rgba(118, 185, 0, 0.2); - margin-left: 0.5rem; -} - -.more-tags, -.more-categories { - font-size: 0.75rem; - color: var(--pst-color-text-muted); - font-style: italic; - margin-left: 0.25rem; -} - -.result-tags, -.result-categories { - display: flex; - flex-wrap: wrap; - gap: 0.25rem; - align-items: center; -} - -/* Badge styles */ -.badge { - display: inline-flex; - align-items: center; - padding: 0.375rem 0.5rem; - font-size: 0.75rem; - font-weight: 500; - border-radius: 0.25rem; - text-decoration: none; -} - -.bg-secondary { - background-color: var(--pst-color-text-muted) !important; - color: var(--pst-color-background) !important; -} - -.bg-info { - background-color: rgba(118, 185, 0, 0.9) !important; - color: var(--pst-color-background) !important; -} - -.bg-light { - background-color: transparent !important; - color: var(--pst-color-text-muted) !important; - border: 1px solid var(--pst-color-on-surface) !important; -} - -/* Metadata badges */ -.metadata-badge { - display: inline-flex; - align-items: center; - padding: 0.2rem 0.5rem; - margin-right: 0.5rem; - margin-bottom: 0.25rem; - font-size: 0.75rem; - font-weight: 500; - border-radius: 0.375rem; - border: 1px solid; - cursor: help; - transition: all 0.2s ease; -} - -.persona-badge { - background-color: #e8f5e8; - color: #2d5a2d; - border-color: #c3e6c3; -} - -.difficulty-badge { - background-color: #fff3cd; - color: #856404; - border-color: #ffeaa7; -} - -.modality-badge { - background-color: #e2f3ff; - color: #0c5460; - border-color: #b8daff; -} - -.metadata-badge:hover { - transform: translateY(-1px); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -/* Clickable badge styles */ -.clickable-badge { - cursor: pointer; - transition: all 0.2s ease; - user-select: none; -} - -.clickable-badge:hover { - transform: translateY(-1px); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); - filter: brightness(1.1); -} - -.clickable-badge:active { - transform: translateY(0); -} - -.result-tag.clickable-badge:hover { - background-color: var(--nv-color-green); - color: var(--pst-color-background); - border-color: var(--nv-color-green); -} - -/* Active filter display */ -.active-filters-display { - background-color: var(--pst-color-surface); - border: 1px solid var(--pst-color-on-surface); - border-radius: 0.375rem; - padding: 0.75rem; -} - -.active-filter-badge { - display: inline-flex; - align-items: center; - padding: 0.2rem 0.5rem; - margin-right: 0.5rem; - font-size: 0.75rem; - font-weight: 500; - border-radius: 0.25rem; - background-color: var(--nv-color-green); - color: var(--pst-color-background); - border: 1px solid var(--nv-color-green); -} - -/* Utility classes for layout */ -.mb-1 { - margin-bottom: 0.25rem !important; -} - -.mb-2 { - margin-bottom: 0.5rem !important; -} - -.mb-3 { - margin-bottom: 1rem !important; -} - -.mb-4 { - margin-bottom: 1.5rem !important; -} - -.mt-1 { - margin-top: 0.25rem !important; -} - -.mt-3 { - margin-top: 1rem !important; -} - -.me-1 { - margin-right: 0.25rem !important; -} - -.me-2 { - margin-right: 0.5rem !important; -} - -.me-3 { - margin-right: 1rem !important; -} - -.ms-2 { - margin-left: 0.5rem !important; -} - -.ms-4 { - margin-left: 1.5rem !important; -} - -.d-flex { - display: flex !important; -} - -.align-items-center { - align-items: center !important; -} - -.align-items-start { - align-items: flex-start !important; -} - -.flex-grow-1 { - flex-grow: 1 !important; -} - -.flex-wrap { - flex-wrap: wrap !important; -} - -.gap-2 { - gap: 0.5rem !important; -} - -.text-decoration-none { - text-decoration: none !important; -} - -.text-center { - text-align: center !important; -} - -.text-muted { - color: var(--pst-color-text-muted) !important; -} - -.py-4 { - padding-top: 1.5rem !important; - padding-bottom: 1.5rem !important; -} - -.p-2 { - padding: 0.5rem !important; -} - -.border { - border: 1px solid var(--pst-color-on-surface) !important; -} - -.rounded { - border-radius: 0.25rem !important; -} - -.small { - font-size: 0.875rem !important; -} - -/* Empty and Error States */ -.no-results { - text-align: center; - padding: 3rem 1rem; - color: var(--pst-color-text-muted); - font-family: var(--pst-font-family-base); -} - -.no-results h3 { - color: var(--pst-color-heading); - font-family: var(--pst-font-family-heading); - font-weight: var(--pst-font-weight-heading); - font-size: var(--pst-font-size-h3); - margin: 0 0 1rem 0; -} - -.no-results p { - font-size: 1.125rem; - line-height: 1.6; - margin: 0; -} - -.error-message { - background-color: var(--pst-color-surface); - border: 1px solid var(--pst-color-on-surface); - border-left: 4px solid var(--nv-color-green); - border-radius: 0.5rem; - padding: 1rem; - margin: 1rem 0; - color: var(--pst-color-text-base); - font-family: var(--pst-font-family-base); -} - -/* Search Highlighting */ -.search-highlight, -mark { - background-color: rgba(118, 185, 0, 0.2); - color: var(--pst-color-text-base); - padding: 0.0625rem 0.125rem; - border-radius: 0.125rem; - font-weight: 400; - border: 1px solid rgba(118, 185, 0, 0.3); -} - -/* Section-specific icon colors and styles */ -.section-badge.getting-started { - background: linear-gradient(135deg, var(--nv-color-green), var(--nv-color-green-2)); - color: var(--pst-color-background); - border-color: var(--nv-color-green); -} - -.section-badge.admin { - background-color: var(--pst-color-surface); - color: var(--pst-color-text-base); -} - -.section-badge.reference { - background-color: var(--pst-color-surface); - color: var(--pst-color-text-base); -} - -.section-badge.tutorial { - background-color: var(--pst-color-surface); - color: var(--pst-color-text-base); -} - -/* Empty state icons and messaging */ -.search-empty-state, -.search-no-results { - text-align: center; - padding: 2rem; - color: var(--pst-color-text-muted); - font-family: var(--pst-font-family-base); -} - -.search-empty-state i, -.search-no-results i { - font-size: 3rem; - color: var(--pst-color-text-muted); - margin-bottom: 1rem; - display: block; -} - -.search-empty-state h4, -.search-no-results h4 { - color: var(--pst-color-heading); - font-family: var(--pst-font-family-heading); - font-size: var(--pst-font-size-h4); - margin-bottom: 0.5rem; -} - -.search-empty-state p, -.search-no-results p { - color: var(--pst-color-text-muted); - font-size: 1rem; - line-height: 1.5; - margin-bottom: 1rem; -} - -/* Responsive Design */ -@media (max-width: 768px) { - .search-result { - padding: 1rem; - margin-bottom: 1rem; - } - - .result-header { - flex-direction: column; - gap: 0.75rem; - } - - .section-icon { - width: 2.5rem; - height: 2.5rem; - font-size: 1rem; - } - - .result-title { - font-size: var(--pst-font-size-h5); - } - - .result-meta { - flex-direction: column; - align-items: flex-start; - gap: 0.5rem; - } - - .section-links { - padding: 0.5rem; - } - - .section-link { - padding: 0.375rem 0.5rem; - font-size: 0.8125rem; - } - - #enhanced-search-page-input { - font-size: 1rem; - padding: 0.875rem 1rem; - } -} - -/* High contrast mode support */ -@media (prefers-contrast: high) { - .search-result { - border-width: 2px; - } - - .search-result:hover { - border-width: 3px; - } - - .search-highlight, - mark { - outline: 1px solid var(--pst-color-text-base); - } -} - -/* Reduced motion support */ -@media (prefers-reduced-motion: reduce) { - - .search-result, - .section-link, - #enhanced-search-page-input, - .search-result::before { - transition: none; - } - - .spinner { - animation: none; - } -} - -/* Print styles */ -@media print { - .search-result { - break-inside: avoid; - box-shadow: none; - border: 1px solid; - margin-bottom: 1rem; - background: transparent !important; - } - - .section-icon { - background: transparent !important; - border: 1px solid; - } - - .section-link { - text-decoration: underline !important; - } - - .search-highlight, - mark { - background: transparent !important; - text-decoration: underline; - font-weight: bold; - } -} - -/* Focus states for accessibility */ -#enhanced-search-page-input:focus-visible { - outline: 2px solid var(--nv-color-green); - outline-offset: 2px; -} - -.section-link:focus-visible { - outline: 2px solid var(--nv-color-green); - outline-offset: 2px; -} - -.result-title a:focus-visible { - outline: 2px solid var(--nv-color-green); - outline-offset: 2px; - border-radius: 0.125rem; -} - -/* Dark theme support */ -html[data-theme="dark"] .search-result { - background: var(--pst-color-surface-200, #1f2937); -} - -html[data-theme="dark"] .search-result:hover { - background: var(--pst-color-surface-300, #111827); -} - -html[data-theme="dark"] .search-results-header h3 { - color: var(--pst-color-text-base, #f9fafb); -} - -/* Accessibility enhancements */ -@media (prefers-reduced-motion: reduce) { - - .search-result, - .section-link, - #enhanced-search-page-input { - transition: none; - } -} - -@media (prefers-contrast: high) { - .search-result { - border-color: var(--pst-color-text-base); - } - - .search-highlight, - mark { - background: var(--nv-color-green); - color: var(--pst-color-background); - } -} - -/* AI Assistant container styling */ -.ai-assistant-container { - border: 1px solid var(--pst-color-border); - border-radius: var(--pst-border-radius); - background: var(--pst-color-surface); - padding: 1rem; - margin-top: 1.5rem; -} - -.ai-assistant-container .ai-loading { - text-align: center; - padding: 2rem; - color: var(--pst-color-text-muted); -} - -.ai-assistant-container .ai-response { - line-height: 1.6; -} - -.ai-assistant-container .ai-error { - color: var(--pst-color-danger); - background: var(--pst-color-danger-bg); - padding: 1rem; - border-radius: var(--pst-border-radius); - border-left: 4px solid var(--pst-color-danger); -} - -/* AI Assistant dark theme support */ -html[data-theme="dark"] .ai-assistant-container { - background: var(--pst-color-surface-200, #1f2937); - border-color: var(--pst-color-border-dark, #374151); -} - -/* ===== TOPIC BADGES ===== */ -.result-topics { - display: flex; - flex-wrap: wrap; - gap: 0.25rem; - align-items: center; -} - -.topic-badge { - display: inline-flex; - align-items: center; - padding: 0.25rem 0.5rem; - margin-right: 0.25rem; - font-size: 0.75rem; - background: var(--topic-bg, #e8f5e9); - color: var(--topic-text, #2e7d32); - border-radius: 4px; - cursor: pointer; - transition: all 0.15s ease-in-out; - border: 1px solid rgba(46, 125, 50, 0.2); -} - -.topic-badge:hover { - background: var(--topic-bg-hover, #c8e6c9); - transform: translateY(-1px); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.topic-badge:active { - transform: translateY(0); -} - -/* Dark theme topic badges */ -html[data-theme="dark"] .topic-badge { - background: rgba(118, 185, 0, 0.15); - color: var(--nv-color-green); - border-color: rgba(118, 185, 0, 0.3); -} - -html[data-theme="dark"] .topic-badge:hover { - background: rgba(118, 185, 0, 0.25); -} - -/* More Topics Indicator */ -.more-topics { - font-size: 0.75rem; - color: var(--pst-color-text-muted); - padding: 0.25rem; - font-style: italic; -} - -/* ===== RESULT BREAKDOWN ===== */ -.result-breakdown { - margin-left: 0.5rem; - font-size: 0.875rem; - color: var(--pst-color-text-muted); -} - -.result-breakdown::before { - content: '— '; -} - -/* ===== KEYBOARD NAVIGATION FOCUS STATES ===== */ -.search-result.focused { - outline: 2px solid var(--nv-color-green, #76b900); - outline-offset: 2px; - border-radius: 8px; - background-color: rgba(118, 185, 0, 0.05); -} - -.search-result:focus-visible { - outline: 2px solid var(--nv-color-green, #76b900); - outline-offset: 2px; -} - -/* Animation for focus transition */ -.search-result { - transition: outline 0.15s ease-in-out, background-color 0.15s ease-in-out, transform 0.2s ease-in-out, border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out; -} - -/* Dark theme focus states */ -html[data-theme="dark"] .search-result.focused { - background-color: rgba(118, 185, 0, 0.1); - outline-color: var(--nv-color-green); -} - -/* ===== EXTENDED FILTER GRID RESPONSIVE ===== */ -@media (max-width: 1200px) { - .filter-grid { - grid-template-columns: repeat(3, 1fr); - } -} - -/* Dark theme filter enhancements */ -html[data-theme="dark"] .search-controls-container { - background: linear-gradient(to bottom, var(--pst-color-surface-200, #1f2937), var(--pst-color-surface-300, #111827)); - border-color: var(--pst-color-border-dark, #374151); -} - -html[data-theme="dark"] .filter-header { - border-bottom-color: var(--pst-color-border-dark, #374151); -} - -html[data-theme="dark"] .filter-select { - background-color: var(--pst-color-surface-200, #1f2937); - border-color: var(--pst-color-border-dark, #374151); -} - -html[data-theme="dark"] .filter-select:hover:not(:focus) { - background-color: var(--pst-color-surface-300, #111827); -} - -html[data-theme="dark"] .filter-clear-btn { - border-color: var(--pst-color-border-dark, #374151); -} - -html[data-theme="dark"] .filter-clear-btn:hover { - background-color: var(--pst-color-surface-300, #111827); -} - -html[data-theme="dark"] .search-input-field, -html[data-theme="dark"] #enhanced-search-page-input { - background-color: var(--pst-color-surface-200, #1f2937); - border-color: var(--pst-color-border-dark, #374151); -} - -html[data-theme="dark"] .filter-select-wrapper.has-value { - --select-bg-color: rgba(118, 185, 0, 0.1); -} - -/* ===== ACCESSIBILITY SKIP LINK ===== */ -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} - -/* Visual indicator for keyboard users */ -.search-results-list:focus-within { - outline: 1px dashed var(--pst-color-text-muted); - outline-offset: 4px; - border-radius: 8px; -} - -/* Reduced motion support for keyboard navigation */ -@media (prefers-reduced-motion: reduce) { - .search-result.focused { - transition: none; - } -} diff --git a/docs/_ext/search_assets/main.js b/docs/_ext/search_assets/main.js deleted file mode 100644 index 31140ef1f..000000000 --- a/docs/_ext/search_assets/main.js +++ /dev/null @@ -1,197 +0,0 @@ -/** - * Enhanced Search Main Entry Point - * Loads search engine and page manager for enhanced search page - * Does NOT interfere with default search behavior - */ - -// Prevent multiple initializations -if (typeof window.EnhancedSearch !== 'undefined') { -} else { - -// Import modules (will be loaded dynamically) -class EnhancedSearch { - constructor(options = {}) { - this.options = { - placeholder: options.placeholder || 'Search documentation...', - maxResults: options.maxResults || 20, - minQueryLength: 2, - highlightClass: 'search-highlight', - ...options - }; - - this.isLoaded = false; - - // Module instances - this.documentLoader = null; - this.searchEngine = null; - this.searchPageManager = null; - this.utils = null; - - this.init(); - } - - async init() { - try { - // Load required modules - await this.loadModules(); - - // Initialize core modules - this.utils = new Utils(); - this.documentLoader = new DocumentLoader(); - this.searchEngine = new SearchEngine(this.utils); - - // Load documents and initialize search engine (always needed) - await this.documentLoader.loadDocuments(); - await this.searchEngine.initialize(this.documentLoader.getDocuments()); - - // Check if we're on the search page - const isSearchPage = this.isSearchPage(); - - if (isSearchPage) { - this.searchPageManager = new SearchPageManager(); - } - - this.isLoaded = true; - } catch (error) { - this.fallbackToDefaultSearch(); - } - } - - isSearchPage() { - return window.location.pathname.includes('/search') || - window.location.pathname.includes('/search.html') || - window.location.pathname.endsWith('search/') || - document.querySelector('#enhanced-search-page-input') !== null || - document.querySelector('#enhanced-search-page-results') !== null; - } - - async loadModules() { - const moduleNames = [ - 'Utils', - 'DocumentLoader', - 'SearchEngine', - 'SearchPageManager' - ]; - - // Load modules with smart path resolution - const modulePromises = moduleNames.map(name => - this.loadModuleWithFallback(name) - ); - - await Promise.all(modulePromises); - } - - async loadModuleWithFallback(moduleName) { - const possiblePaths = this.getModulePaths(moduleName); - - for (const path of possiblePaths) { - try { - await this.loadModule(path); - return; - } catch (error) { - // Continue to next path - } - } - - throw new Error(`Failed to load module ${moduleName} from any path`); - } - - getModulePaths(moduleName) { - const fileName = `${moduleName}.js`; - - // Calculate nesting level to determine correct _static path - const pathParts = window.location.pathname.split('/').filter(part => part.length > 0); - const htmlFile = pathParts[pathParts.length - 1]; - - // Remove the HTML file from the count if it exists - let nestingLevel = pathParts.length; - if (htmlFile && htmlFile.endsWith('.html')) { - nestingLevel--; - } - - // Build the correct _static path based on nesting level - const staticPrefix = nestingLevel > 0 ? '../'.repeat(nestingLevel) : './'; - const staticPath = `${staticPrefix}_static`; - - // Search assets only has modules directory - const moduleDir = 'modules'; - - // Generate paths in order of likelihood - const paths = []; - - // 1. Most likely path based on calculated nesting - paths.push(`${staticPath}/${moduleDir}/${fileName}`); - - // 2. Fallback static paths (for different nesting scenarios) - paths.push(`_static/${moduleDir}/${fileName}`); - paths.push(`./_static/${moduleDir}/${fileName}`); - if (nestingLevel > 1) { - paths.push(`../_static/${moduleDir}/${fileName}`); - } - - // 3. Legacy fallback paths - paths.push(`./modules/${fileName}`); - paths.push(`../modules/${fileName}`); - paths.push(`modules/${fileName}`); - - return paths; - } - - async loadModule(src) { - return new Promise((resolve, reject) => { - const script = document.createElement('script'); - script.src = src; - script.onload = resolve; - script.onerror = () => reject(new Error(`Failed to load module: ${src}`)); - document.head.appendChild(script); - }); - } - - // Public API methods - search(query) { - if (!this.searchEngine) { - return []; - } - - return this.searchEngine.search(query); - } - - renderResults(results, query) { - // Use SearchPageManager for search page rendering - return ''; - } - - fallbackToDefaultSearch() { - // Don't interfere with default search - just fallback - } - - getDocuments() { - return this.documentLoader ? this.documentLoader.getDocuments() : []; - } - - get documents() { - return this.getDocuments(); - } - - getSearchEngine() { - return this.searchEngine; - } - - getOptions() { - return this.options; - } -} - -// Initialize the enhanced search system -window.EnhancedSearch = EnhancedSearch; - -// Auto-initialize -document.addEventListener('DOMContentLoaded', function() { - // Create the global instance - window.enhancedSearchInstance = new EnhancedSearch({ - placeholder: 'Search NVIDIA documentation...', - maxResults: 50 - }); -}); - -} // End of duplicate prevention check diff --git a/docs/_ext/search_assets/modules/DocumentLoader.js b/docs/_ext/search_assets/modules/DocumentLoader.js deleted file mode 100644 index a15e55c1f..000000000 --- a/docs/_ext/search_assets/modules/DocumentLoader.js +++ /dev/null @@ -1,239 +0,0 @@ -/** - * DocumentLoader Module - * Handles loading and managing search documents from JSON index - */ - -class DocumentLoader { - constructor() { - this.documents = {}; - this.isLoaded = false; - } - - /** - * Load documents from JSON index files - */ - async loadDocuments() { - try { - const data = await this.fetchDocumentData(); - this.processDocuments(data); - this.isLoaded = true; - console.log(`✅ Document loader initialized with ${Object.keys(this.documents).length} documents`); - } catch (error) { - console.error('Failed to load search documents:', error); - throw error; - } - } - - /** - * Fetch document data from various possible paths - */ - async fetchDocumentData() { - // Try different paths to account for different page depths - const possiblePaths = [ - './index.json', - '../index.json', - '../../index.json', - '../../../index.json' - ]; - - for (const path of possiblePaths) { - try { - const response = await fetch(path); - if (response.ok) { - const data = await response.json(); - console.log(`✅ Loaded search index from: ${path}`); - return data; - } - } catch (error) { - console.log(`❌ Failed to load from ${path}: ${error.message}`); - } - } - - throw new Error('Failed to load search data from any path'); - } - - /** - * Process and filter documents from raw data - * Supports three formats: - * 1. Array of documents (new format): [{ id, title, ... }, ...] - * 2. Object with children (legacy): { children: [...] } - * 3. Single document (fallback): { id, title, ... } - */ - processDocuments(data) { - let allDocs; - if (Array.isArray(data)) { - // New format: root is an array of documents - allDocs = data; - } else if (data.children) { - // Legacy format: object with children array - allDocs = data.children; - } else { - // Fallback: single document - allDocs = [data]; - } - - // Filter out problematic documents - const filteredDocs = allDocs.filter(doc => this.isValidDocument(doc)); - - // Store documents by ID - filteredDocs.forEach(doc => { - this.documents[doc.id] = this.sanitizeDocument(doc); - }); - - console.log(`Processed ${filteredDocs.length} documents (filtered from ${allDocs.length} total)`); - } - - /** - * Check if a document is valid for indexing - */ - isValidDocument(doc) { - const docId = doc.id || ''; - return !docId.toLowerCase().includes('readme') && - !docId.startsWith('_') && - doc.title && - doc.content; - } - - /** - * Sanitize document content for safe indexing - * Supports both new schema fields and legacy fields - * Preserves dynamic facets as-is - */ - sanitizeDocument(doc) { - const sanitized = { - ...doc, - title: this.sanitizeText(doc.title, 200), - // Add description as separate indexed field (for improved search relevance) - description: this.sanitizeText(doc.description, 300), - content: this.sanitizeText(doc.content, 5000), - summary: this.sanitizeText(doc.summary, 500), - headings: this.sanitizeHeadings(doc.headings), - headings_text: this.sanitizeText(doc.headings_text, 1000), - keywords: this.sanitizeArray(doc.keywords, 300), - tags: this.sanitizeArray(doc.tags, 200), - // Support both topics (new) and categories (legacy) - topics: this.sanitizeArray(doc.topics || doc.categories, 200), - // Support both audience (new) and personas (legacy) - audience: this.sanitizeArray(doc.audience || doc.personas, 200), - // Content type and difficulty - content_type: this.sanitizeText(doc.content_type, 50), - difficulty: this.sanitizeText(doc.difficulty, 50), - doc_type: this.sanitizeText(doc.doc_type, 50), - section_path: this.sanitizeArray(doc.section_path, 200), - author: this.sanitizeText(doc.author, 100) - }; - - // Preserve facets object (dynamic, user-defined keys) - if (doc.facets && typeof doc.facets === 'object') { - sanitized.facets = this.sanitizeFacets(doc.facets); - } - - // Preserve legacy flat modality if present and no facets.modality - if (doc.modality && (!doc.facets || !doc.facets.modality)) { - sanitized.modality = this.sanitizeText(doc.modality, 50); - } - - return sanitized; - } - - /** - * Sanitize facets object (dynamic keys with string or array values) - */ - sanitizeFacets(facets) { - const sanitized = {}; - Object.entries(facets).forEach(([key, value]) => { - if (Array.isArray(value)) { - sanitized[key] = value.map(v => String(v).substring(0, 100)); - } else if (value) { - sanitized[key] = String(value).substring(0, 100); - } - }); - return sanitized; - } - - /** - * Sanitize text content with length limits - */ - sanitizeText(text, maxLength) { - if (!text || typeof text !== 'string') return ''; - return text.substring(0, maxLength); - } - - /** - * Sanitize array content - */ - sanitizeArray(arr, maxLength) { - if (!Array.isArray(arr)) return []; - return arr.map(item => String(item)).join(' ').substring(0, maxLength); - } - - /** - * Sanitize headings array - */ - sanitizeHeadings(headings) { - if (!Array.isArray(headings)) return []; - return headings.map(heading => ({ - text: this.sanitizeText(heading.text, 200), - level: Number(heading.level) || 1 - })); - } - - /** - * Get all loaded documents - */ - getDocuments() { - return this.documents; - } - - /** - * Get a specific document by ID - */ - getDocument(id) { - return this.documents[id]; - } - - /** - * Get document count - */ - getDocumentCount() { - return Object.keys(this.documents).length; - } - - /** - * Check if documents are loaded - */ - isReady() { - return this.isLoaded && Object.keys(this.documents).length > 0; - } - - /** - * Get documents as array for indexing - */ - getDocumentsArray() { - return Object.values(this.documents); - } - - /** - * Filter documents by criteria - */ - filterDocuments(filterFn) { - return this.getDocumentsArray().filter(filterFn); - } - - /** - * Get document statistics - */ - getStatistics() { - const docs = this.getDocumentsArray(); - return { - totalDocuments: docs.length, - documentsWithSummary: docs.filter(d => d.summary).length, - documentsWithHeadings: docs.filter(d => d.headings && d.headings.length > 0).length, - documentsWithTags: docs.filter(d => d.tags && d.tags.length > 0).length, - averageContentLength: docs.reduce((sum, d) => sum + (d.content?.length || 0), 0) / docs.length - }; - } -} - -// Make DocumentLoader available globally -window.DocumentLoader = DocumentLoader; diff --git a/docs/_ext/search_assets/modules/EventHandler.js b/docs/_ext/search_assets/modules/EventHandler.js deleted file mode 100644 index 31cba430f..000000000 --- a/docs/_ext/search_assets/modules/EventHandler.js +++ /dev/null @@ -1,298 +0,0 @@ -/** - * EventHandler Module - * Handles keyboard shortcuts and event management for the search interface - */ - -class EventHandler { - constructor(enhancedSearch) { - this.enhancedSearch = enhancedSearch; - this.searchInterface = enhancedSearch.searchInterface; - this.resultRenderer = enhancedSearch.resultRenderer; - this.searchEngine = enhancedSearch.searchEngine; - this.utils = enhancedSearch.utils; - - // Track bound event listeners for cleanup - this.boundListeners = new Map(); - - // Debounced search function - this.debouncedSearch = this.utils.debounce(this.handleSearch.bind(this), 200); - } - - /** - * Bind all event listeners - */ - bindEvents() { - this.bindInputEvents(); - this.bindModalEvents(); - this.bindGlobalEvents(); - console.log('✅ Event handlers bound'); - } - - /** - * Bind input-related events - */ - bindInputEvents() { - const input = this.searchInterface.getInput(); - if (!input) return; - - // Search input - const inputHandler = (e) => this.debouncedSearch(e); - input.addEventListener('input', inputHandler); - this.boundListeners.set('input', inputHandler); - - // Keyboard navigation - const keydownHandler = (e) => this.handleKeyDown(e); - input.addEventListener('keydown', keydownHandler); - this.boundListeners.set('keydown', keydownHandler); - } - - /** - * Bind page-specific events (replaces modal events) - */ - bindModalEvents() { - // Check if we're on the search page - if (!this.searchInterface.isSearchPage()) { - return; - } - - // Get query parameter if we're on search page - const urlParams = new URLSearchParams(window.location.search); - const query = urlParams.get('q'); - - if (query) { - // Perform search immediately with the query from URL - setTimeout(() => { - const input = this.searchInterface.getInput(); - if (input) { - input.value = query; - this.handleSearch({ target: input }); - } - }, 100); - } - } - - /** - * Bind global keyboard shortcuts - */ - bindGlobalEvents() { - const globalKeyHandler = (e) => { - // Ctrl+K or Cmd+K to focus search input - if ((e.ctrlKey || e.metaKey) && e.key === 'k') { - e.preventDefault(); - // Focus the search input if we're on the search page - const searchInput = this.searchInterface.getInput(); - if (searchInput) { - searchInput.focus(); - } else { - // If not on search page, redirect to search page - window.location.href = 'search.html'; - } - return; - } - }; - - document.addEventListener('keydown', globalKeyHandler); - this.boundListeners.set('global', globalKeyHandler); - } - - /** - * Handle search input - */ - async handleSearch(event) { - const query = event.target.value.trim(); - const resultsContainer = this.searchInterface.getResultsContainer(); - - if (query.length < this.enhancedSearch.options.minQueryLength) { - this.searchInterface.showEmptyState(); - this.searchInterface.clearStats(); - return; - } - - try { - // Show loading state - this.resultRenderer.renderLoading(resultsContainer); - - // Perform search - const results = this.searchEngine.search(query, this.enhancedSearch.options.maxResults); - const count = results.length; - - // Render results - this.resultRenderer.render(results, query, resultsContainer); - - // Update stats - this.searchInterface.updateStats(query, count); - - // Emit search event for AI Assistant extension if available - this.emitSearchEvent(query, results, count); - - } catch (error) { - console.error('Search error:', error); - this.resultRenderer.renderError(resultsContainer, 'Search temporarily unavailable'); - this.searchInterface.clearStats(); - } - } - - /** - * Handle keyboard navigation - */ - handleKeyDown(event) { - const resultsContainer = this.searchInterface.getResultsContainer(); - - switch (event.key) { - case 'ArrowDown': - event.preventDefault(); - this.resultRenderer.selectNext(resultsContainer); - break; - - case 'ArrowUp': - event.preventDefault(); - this.resultRenderer.selectPrevious(resultsContainer); - break; - - case 'Enter': - event.preventDefault(); - this.resultRenderer.activateSelected(resultsContainer); - break; - - case 'Escape': - event.preventDefault(); - this.enhancedSearch.hide(); - break; - } - } - - /** - * Emit search event for other extensions - */ - emitSearchEvent(query, results, count) { - if (window.AIAssistant && window.aiAssistantInstance) { - const searchEvent = new CustomEvent('enhanced-search-results', { - detail: { query, results, count } - }); - document.dispatchEvent(searchEvent); - } - } - - /** - * Handle window resize - */ - handleResize() { - // Adjust modal positioning if needed - const modal = this.searchInterface.getModal(); - if (modal && this.searchInterface.isModalVisible()) { - // Could add responsive adjustments here - } - } - - /** - * Handle focus management - */ - handleFocus(event) { - // Trap focus within modal when visible - if (this.searchInterface.isModalVisible()) { - const modal = this.searchInterface.getModal(); - const focusableElements = modal.querySelectorAll( - 'button, input, select, textarea, [tabindex]:not([tabindex="-1"])' - ); - - const firstFocusable = focusableElements[0]; - const lastFocusable = focusableElements[focusableElements.length - 1]; - - if (event.key === 'Tab') { - if (event.shiftKey) { - // Shift + Tab - if (document.activeElement === firstFocusable) { - event.preventDefault(); - lastFocusable.focus(); - } - } else { - // Tab - if (document.activeElement === lastFocusable) { - event.preventDefault(); - firstFocusable.focus(); - } - } - } - } - } - - /** - * Bind additional event listeners - */ - bindAdditionalEvents() { - // Window resize - const resizeHandler = this.utils.debounce(() => this.handleResize(), 100); - window.addEventListener('resize', resizeHandler); - this.boundListeners.set('resize', resizeHandler); - - // Focus trap - const focusHandler = (e) => this.handleFocus(e); - document.addEventListener('keydown', focusHandler); - this.boundListeners.set('focus', focusHandler); - } - - /** - * Unbind all event listeners - */ - unbindEvents() { - // Remove input events - const input = this.searchInterface.getInput(); - if (input && this.boundListeners.has('input')) { - input.removeEventListener('input', this.boundListeners.get('input')); - input.removeEventListener('keydown', this.boundListeners.get('keydown')); - } - - // Remove modal events - const closeBtn = this.searchInterface.getCloseButton(); - if (closeBtn && this.boundListeners.has('close')) { - closeBtn.removeEventListener('click', this.boundListeners.get('close')); - } - - const backdrop = this.searchInterface.getBackdrop(); - if (backdrop && this.boundListeners.has('backdrop')) { - backdrop.removeEventListener('click', this.boundListeners.get('backdrop')); - } - - // Remove global events - if (this.boundListeners.has('global')) { - document.removeEventListener('keydown', this.boundListeners.get('global')); - } - - if (this.boundListeners.has('resize')) { - window.removeEventListener('resize', this.boundListeners.get('resize')); - } - - if (this.boundListeners.has('focus')) { - document.removeEventListener('keydown', this.boundListeners.get('focus')); - } - - // Clear listeners map - this.boundListeners.clear(); - - console.log('✅ Event handlers unbound'); - } - - /** - * Get event handler statistics - */ - getStatistics() { - return { - boundListeners: this.boundListeners.size, - modalVisible: this.searchInterface.isModalVisible(), - hasInput: !!this.searchInterface.getInput(), - hasModal: !!this.searchInterface.getModal() - }; - } - - /** - * Check if events are properly bound - */ - isReady() { - return this.boundListeners.size > 0 && - this.searchInterface.getInput() !== null && - this.searchInterface.getModal() !== null; - } -} - -// Make EventHandler available globally -window.EventHandler = EventHandler; diff --git a/docs/_ext/search_assets/modules/ResultRenderer.js b/docs/_ext/search_assets/modules/ResultRenderer.js deleted file mode 100644 index 5a173a241..000000000 --- a/docs/_ext/search_assets/modules/ResultRenderer.js +++ /dev/null @@ -1,263 +0,0 @@ -/** - * ResultRenderer Module - * Handles rendering of search results in the interface - */ - -class ResultRenderer { - constructor(options, utils) { - this.options = options; - this.utils = utils; - } - - /** - * Render search results - */ - render(results, query, container) { - if (!container) { - console.warn('No container provided for rendering results'); - return; - } - - if (results.length === 0) { - container.innerHTML = this.renderNoResults(query); - return; - } - - const html = results.map((result, index) => { - const isSelected = index === 0; - return this.renderResultItem(result, query, isSelected); - }).join(''); - - container.innerHTML = `
${html}
`; - - // Bind click events - this.bindResultEvents(container, results); - } - - /** - * Render a single result item - */ - renderResultItem(result, query, isSelected = false) { - const title = this.utils.highlightText(result.title || 'Untitled', query); - const summary = this.utils.highlightText(result.summary || result.content?.substring(0, 200) || '', query); - const breadcrumb = this.utils.generateBreadcrumb(result.id); - - // Render matching sections - const sectionsHtml = this.renderMatchingSections(result, query); - - // Show multiple matches indicator - const multipleMatchesIndicator = result.totalMatches > 1 - ? `${result.totalMatches} matches` - : ''; - - return ` -
-
-
${title} ${multipleMatchesIndicator}
-
${summary}...
- ${sectionsHtml} -
- ${breadcrumb} - ${result.tags ? `${this.utils.safeArray(result.tags).slice(0, 3).map(tag => `${tag}`).join('')}` : ''} -
-
-
- -
-
- `; - } - - /** - * Render matching sections within a result - */ - renderMatchingSections(result, query) { - if (!result.matchingSections || result.matchingSections.length <= 1) { - return ''; - } - - // Show only the first few sections to avoid overwhelming - const sectionsToShow = result.matchingSections.slice(0, 4); - const hasMore = result.matchingSections.length > 4; - - const sectionsHtml = sectionsToShow.map(section => { - const icon = this.utils.getSectionIcon(section.type, section.level); - const sectionText = this.utils.highlightText(section.text, query); - const anchor = section.anchor ? `#${section.anchor}` : ''; - - return ` -
- ${icon} ${sectionText} -
- `; - }).join(''); - - const moreIndicator = hasMore - ? `
+${result.matchingSections.length - 4} more sections
` - : ''; - - return ` -
- ${sectionsHtml} - ${moreIndicator} -
- `; - } - - /** - * Render no results state - */ - renderNoResults(query) { - return ` -
- -

No results found for "${this.utils.escapeHtml(query)}"

-
- Try: -
    -
  • Checking for typos
  • -
  • Using different or more general terms
  • -
  • Using fewer keywords
  • -
-
-
- `; - } - - /** - * Bind click events to result items - */ - bindResultEvents(container, results) { - container.querySelectorAll('.search-result-item').forEach((item, index) => { - const result = results[index]; - - // Main item click - go to document - item.addEventListener('click', (e) => { - // Don't trigger if clicking on a section - if (e.target.closest('.search-result-section')) { - return; - } - - const url = item.dataset.url; - window.location.href = url; - }); - - // Section clicks - go to specific section - item.querySelectorAll('.search-result-section').forEach(sectionEl => { - sectionEl.addEventListener('click', (e) => { - e.stopPropagation(); - const anchor = sectionEl.dataset.anchor; - const baseUrl = item.dataset.url; - window.location.href = baseUrl + anchor; - }); - }); - }); - } - - /** - * Get result items from container - */ - getResultItems(container) { - return container.querySelectorAll('.search-result-item'); - } - - /** - * Get selected result item - */ - getSelectedResult(container) { - return container.querySelector('.search-result-item.selected'); - } - - /** - * Select next result item - */ - selectNext(container) { - const results = this.getResultItems(container); - const selected = this.getSelectedResult(container); - - if (results.length === 0) return; - - if (!selected) { - results[0].classList.add('selected'); - return; - } - - const currentIndex = Array.from(results).indexOf(selected); - selected.classList.remove('selected'); - - const nextIndex = (currentIndex + 1) % results.length; - results[nextIndex].classList.add('selected'); - results[nextIndex].scrollIntoView({ block: 'nearest' }); - } - - /** - * Select previous result item - */ - selectPrevious(container) { - const results = this.getResultItems(container); - const selected = this.getSelectedResult(container); - - if (results.length === 0) return; - - if (!selected) { - results[results.length - 1].classList.add('selected'); - return; - } - - const currentIndex = Array.from(results).indexOf(selected); - selected.classList.remove('selected'); - - const prevIndex = currentIndex === 0 ? results.length - 1 : currentIndex - 1; - results[prevIndex].classList.add('selected'); - results[prevIndex].scrollIntoView({ block: 'nearest' }); - } - - /** - * Activate selected result - */ - activateSelected(container) { - const selected = this.getSelectedResult(container); - if (selected) { - selected.click(); - } - } - - /** - * Clear all selections - */ - clearSelection(container) { - const results = this.getResultItems(container); - results.forEach(result => result.classList.remove('selected')); - } - - /** - * Render loading state - */ - renderLoading(container) { - if (container) { - container.innerHTML = ` -
- -

Searching...

-
- `; - } - } - - /** - * Render error state - */ - renderError(container, message = 'Search error occurred') { - if (container) { - container.innerHTML = ` -
- -

${this.utils.escapeHtml(message)}

-
- `; - } - } -} - -// Make ResultRenderer available globally -window.ResultRenderer = ResultRenderer; diff --git a/docs/_ext/search_assets/modules/SearchEngine.js b/docs/_ext/search_assets/modules/SearchEngine.js deleted file mode 100644 index c3e4b777b..000000000 --- a/docs/_ext/search_assets/modules/SearchEngine.js +++ /dev/null @@ -1,817 +0,0 @@ -/** - * SearchEngine Module - * Handles Lunr.js integration and search logic with filtering and grouping - */ - -class SearchEngine { - constructor(utils) { - this.utils = utils; - this.index = null; - this.documents = {}; - this.isInitialized = false; - // Support both new schema (topics, audience) and legacy (categories, personas) - this.topics = new Set(); - this.tags = new Set(); - this.documentTypes = new Set(); - this.audience = new Set(); - this.difficulties = new Set(); - // Dynamic facets - discovered from documents, not predefined - this.facets = {}; // { facetKey: Set of values } - } - - /** - * Initialize the search engine with documents - */ - async initialize(documents) { - try { - await this.loadLunr(); - this.documents = documents; - this.collectMetadata(); - this.buildIndex(); - this.isInitialized = true; - } catch (error) { - throw error; - } - } - - /** - * Collect metadata for filtering using actual frontmatter values - * Supports both new schema (topics, audience) and legacy (categories, personas) - * Dynamically discovers all facet keys from documents - */ - collectMetadata() { - // Clear existing sets - this.topics = new Set(); - this.tags = new Set(); - this.documentTypes = new Set(); - this.audience = new Set(); - this.difficulties = new Set(); - this.facets = {}; // Reset dynamic facets - - Object.values(this.documents).forEach(doc => { - // Collect topics (new schema) or categories (legacy) - const topicsField = doc.topics || doc.categories; - if (topicsField) { - if (Array.isArray(topicsField)) { - topicsField.forEach(topic => this.topics.add(topic)); - } else if (typeof topicsField === 'string') { - topicsField.split(',').forEach(topic => this.topics.add(topic.trim())); - } - } - - // Collect actual frontmatter tags - if (doc.tags) { - if (Array.isArray(doc.tags)) { - doc.tags.forEach(tag => { - // Split space-separated tags and add individually - if (typeof tag === 'string' && tag.includes(' ')) { - tag.split(' ').forEach(individualTag => { - if (individualTag.trim()) { - this.tags.add(individualTag.trim()); - } - }); - } else if (tag && tag.trim()) { - this.tags.add(tag.trim()); - } - }); - } else if (typeof doc.tags === 'string') { - // Handle both comma-separated and space-separated tags - const allTags = doc.tags.includes(',') - ? doc.tags.split(',') - : doc.tags.split(' '); - - allTags.forEach(tag => { - if (tag && tag.trim()) { - this.tags.add(tag.trim()); - } - }); - } - } - - // Use actual content_type from frontmatter (not calculated doc_type) - if (doc.content_type) { - this.documentTypes.add(doc.content_type); - } - - // Collect audience (new schema) or personas (legacy) - const audienceField = doc.audience || doc.personas; - if (audienceField) { - if (Array.isArray(audienceField)) { - audienceField.forEach(aud => this.audience.add(aud)); - } else if (typeof audienceField === 'string') { - this.audience.add(audienceField); - } - } - - if (doc.difficulty) { - this.difficulties.add(doc.difficulty); - } - - // Dynamically discover all facets from documents - if (doc.facets && typeof doc.facets === 'object') { - Object.entries(doc.facets).forEach(([facetKey, facetValue]) => { - // Initialize Set for this facet if not exists - if (!this.facets[facetKey]) { - this.facets[facetKey] = new Set(); - } - // Add value(s) to the facet Set - if (Array.isArray(facetValue)) { - facetValue.forEach(v => this.facets[facetKey].add(v)); - } else if (facetValue) { - this.facets[facetKey].add(facetValue); - } - }); - } - - // Also check for flat facet fields (legacy modality, etc.) - // These get added to facets dynamically - if (doc.modality && !this.facets.modality) { - this.facets.modality = new Set(); - } - if (doc.modality) { - this.facets.modality.add(doc.modality); - } - }); - } - - /** - * Get available filter options using actual frontmatter taxonomy - * Returns both new field names and legacy names for backwards compatibility - * Includes dynamically discovered facets - */ - getFilterOptions() { - // Convert dynamic facets from Sets to sorted arrays - const facetOptions = {}; - Object.entries(this.facets).forEach(([facetKey, facetSet]) => { - facetOptions[facetKey] = Array.from(facetSet).sort(); - }); - - return { - // New schema names - topics: Array.from(this.topics).sort(), - audience: Array.from(this.audience).sort(), - // Legacy names (aliases for backwards compatibility) - categories: Array.from(this.topics).sort(), - personas: Array.from(this.audience).sort(), - // Common fields - tags: Array.from(this.tags).sort(), - documentTypes: Array.from(this.documentTypes).sort(), - difficulties: Array.from(this.difficulties).sort(), - // Dynamic facets (user-defined, discovered from documents) - facets: facetOptions - }; - } - - /** - * Load Lunr.js library if not already loaded - */ - async loadLunr() { - if (typeof lunr === 'undefined') { - await this.utils.loadScript('https://unpkg.com/lunr@2.3.9/lunr.min.js'); - } - } - - /** - * Build the Lunr search index - * Supports both new schema (topics, audience) and legacy (categories, personas) - * - * Field boosting rationale: - * - Title matches are almost always what users want (highest boost) - * - Description (from frontmatter) is hand-crafted summary (high boost) - * - Headings provide structural relevance (medium-high boost) - * - Content gets lowest boost to prevent long documents from dominating - * - Hierarchy: title > description > headings/keywords > tags > content - */ - buildIndex() { - const documentsArray = Object.values(this.documents); - const self = this; - - this.index = lunr(function() { - // Define fields with optimized boosting for documentation search patterns - this.ref('id'); - - // Primary fields - highest relevance - this.field('title', { boost: 10 }); // Title matches most important - this.field('description', { boost: 8 }); // Frontmatter description (hand-crafted) - - // Secondary fields - structural relevance - this.field('keywords', { boost: 7 }); // Explicit keywords - this.field('headings_text', { boost: 5 }); // Section headings - this.field('headings', { boost: 5 }); // Section headings (legacy format) - this.field('tags', { boost: 4 }); // Taxonomy tags - - // Tertiary fields - content matching - this.field('summary', { boost: 3 }); // Summary field - this.field('topics', { boost: 2 }); // Topic categorization - this.field('content', { boost: 1 }); // Full content (low to prevent long docs dominating) - - // Metadata fields - filtering support - this.field('content_type', { boost: 1 }); - this.field('audience', { boost: 1 }); - this.field('difficulty', { boost: 1 }); - this.field('modality', { boost: 1 }); - this.field('section_path', { boost: 1 }); - this.field('author', { boost: 1 }); - - // Add documents to index - documentsArray.forEach((doc) => { - try { - this.add({ - id: doc.id, - title: doc.title || '', - description: doc.description || '', // NEW: separate indexed field - content: (doc.content || '').substring(0, 5000), // Limit content length - summary: doc.summary || '', - headings: self.extractHeadingsText(doc.headings), - headings_text: doc.headings_text || '', - keywords: self.arrayToString(doc.keywords), - tags: self.arrayToString(doc.tags), - // Support both topics (new) and categories (legacy) - topics: self.arrayToString(doc.topics || doc.categories), - content_type: doc.content_type || '', - // Support both audience (new) and personas (legacy) - audience: self.arrayToString(doc.audience || doc.personas), - difficulty: doc.difficulty || '', - modality: doc.modality || '', - section_path: self.arrayToString(doc.section_path), - author: doc.author || '' - }); - } catch (docError) { - // Skip documents that fail to index - } - }, this); - }); - } - - /** - * Convert array to string for indexing - */ - arrayToString(arr) { - if (Array.isArray(arr)) { - return arr.join(' '); - } - return arr || ''; - } - - /** - * Extract text from headings array - */ - extractHeadingsText(headings) { - if (!Array.isArray(headings)) return ''; - return headings.map(h => h.text || '').join(' '); - } - - /** - * Perform search with query and optional filters - */ - search(query, filters = {}, maxResults = 20) { - if (!this.isInitialized || !this.index) { - return []; - } - - if (!query || query.trim().length < 2) { - return []; - } - - try { - // Enhanced search with multiple strategies - const results = this.performMultiStrategySearch(query); - - // Process and enhance results - const enhancedResults = this.enhanceResults(results, query); - - // Apply filters - const filteredResults = this.applyFilters(enhancedResults, filters); - - // Group and rank results - const groupedResults = this.groupResultsByDocument(filteredResults, query); - - return groupedResults.slice(0, maxResults); - - } catch (error) { - return []; - } - } - - /** - * Apply filters to search results - * Supports both new schema (topic, audience) and legacy (category, persona) filter names - * Handles dynamic facet filters - */ - applyFilters(results, filters) { - return results.filter(result => { - // Topic filter (new) or category filter (legacy) - const topicFilter = filters.topic || filters.category; - if (topicFilter && topicFilter !== '') { - const docTopics = this.getDocumentTopics(result); - if (!docTopics.includes(topicFilter)) { - return false; - } - } - - // Tag filter - if (filters.tag && filters.tag !== '') { - const docTags = this.getDocumentTags(result); - if (!docTags.includes(filters.tag)) { - return false; - } - } - - // Document type filter (using actual frontmatter content_type) - if (filters.type && filters.type !== '') { - if (result.content_type !== filters.type) { - return false; - } - } - - // Audience filter (new) or persona filter (legacy) - const audienceFilter = filters.audience || filters.persona; - if (audienceFilter && audienceFilter !== '') { - const docAudience = this.getDocumentAudience(result); - if (!docAudience.includes(audienceFilter)) { - return false; - } - } - - // Difficulty filter - if (filters.difficulty && filters.difficulty !== '') { - if (result.difficulty !== filters.difficulty) { - return false; - } - } - - // Dynamic facet filters (e.g., filters.facets = { modality: 'text-only', framework: 'pytorch' }) - if (filters.facets && typeof filters.facets === 'object') { - for (const [facetKey, facetValue] of Object.entries(filters.facets)) { - if (facetValue && facetValue !== '') { - const docFacetValue = this.getDocumentFacet(result, facetKey); - if (!docFacetValue.includes(facetValue)) { - return false; - } - } - } - } - - // Legacy flat facet filters (e.g., filters.modality directly) - // Check for any filter key that matches a known facet - for (const facetKey of Object.keys(this.facets)) { - if (filters[facetKey] && filters[facetKey] !== '') { - const docFacetValue = this.getDocumentFacet(result, facetKey); - if (!docFacetValue.includes(filters[facetKey])) { - return false; - } - } - } - - return true; - }); - } - - /** - * Get a specific facet value for a document - */ - getDocumentFacet(doc, facetKey) { - // Check nested facets object first - if (doc.facets && doc.facets[facetKey]) { - const value = doc.facets[facetKey]; - return Array.isArray(value) ? value : [value]; - } - // Check flat field (legacy) - if (doc[facetKey]) { - const value = doc[facetKey]; - return Array.isArray(value) ? value : [value]; - } - return []; - } - - /** - * Get topics for a document (supports new schema and legacy categories) - */ - getDocumentTopics(doc) { - const topics = []; - - // From explicit topics (new schema) or categories (legacy) - const topicsField = doc.topics || doc.categories; - if (topicsField) { - if (Array.isArray(topicsField)) { - topics.push(...topicsField); - } else { - topics.push(...topicsField.split(',').map(t => t.trim())); - } - } - - // From section path - if (doc.section_path && Array.isArray(doc.section_path)) { - topics.push(...doc.section_path); - } - - // From document ID path - if (doc.id) { - const pathParts = doc.id.split('/').filter(part => part && part !== 'index'); - topics.push(...pathParts); - } - - return [...new Set(topics)]; // Remove duplicates - } - - /** - * Get categories for a document (legacy alias for getDocumentTopics) - */ - getDocumentCategories(doc) { - return this.getDocumentTopics(doc); - } - - /** - * Get tags for a document - */ - getDocumentTags(doc) { - if (!doc.tags) return []; - - if (Array.isArray(doc.tags)) { - // Handle array of tags that might contain space-separated strings - const flatTags = []; - doc.tags.forEach(tag => { - if (typeof tag === 'string' && tag.includes(' ')) { - // Split space-separated tags - tag.split(' ').forEach(individualTag => { - if (individualTag.trim()) { - flatTags.push(individualTag.trim()); - } - }); - } else if (tag && tag.trim()) { - flatTags.push(tag.trim()); - } - }); - return flatTags; - } - - // Handle string tags - check for both comma and space separation - if (typeof doc.tags === 'string') { - const allTags = []; - const tagString = doc.tags.trim(); - - if (tagString.includes(',')) { - // Comma-separated tags - tagString.split(',').forEach(tag => { - if (tag.trim()) { - allTags.push(tag.trim()); - } - }); - } else { - // Space-separated tags - tagString.split(' ').forEach(tag => { - if (tag.trim()) { - allTags.push(tag.trim()); - } - }); - } - - return allTags; - } - - return []; - } - - - /** - * Get audience for a document (supports new schema and legacy personas) - */ - getDocumentAudience(doc) { - // Support both audience (new) and personas (legacy) - const audienceField = doc.audience || doc.personas; - if (!audienceField) return []; - - if (Array.isArray(audienceField)) { - return audienceField; - } - - return [audienceField]; - } - - /** - * Get personas for a document (legacy alias for getDocumentAudience) - */ - getDocumentPersonas(doc) { - return this.getDocumentAudience(doc); - } - - /** - * Perform search with multiple strategies - */ - performMultiStrategySearch(query) { - const strategies = [ - // Exact phrase search with wildcards - `"${query}" ${query}*`, - // Fuzzy search with wildcards - `${query}* ${query}~2`, - // Individual terms with boost - query.split(/\s+/).map(term => `${term}*`).join(' '), - // Fallback: just the query - query - ]; - - let allResults = []; - const seenIds = new Set(); - - for (const strategy of strategies) { - try { - const results = this.index.search(strategy); - - // Add new results (avoid duplicates) - results.forEach(result => { - if (!seenIds.has(result.ref)) { - seenIds.add(result.ref); - allResults.push({ - ...result, - strategy: strategy - }); - } - }); - - // If we have enough good results, stop - if (allResults.length >= 30) break; - - } catch (strategyError) { - console.warn(`Search strategy failed: ${strategy}`, strategyError); - } - } - - return allResults; - } - - /** - * Enhance search results with document data and apply re-ranking - */ - enhanceResults(results, query) { - const queryLower = query.toLowerCase().trim(); - const queryTerms = queryLower.split(/\s+/); - - return results.map(result => { - const doc = this.documents[result.ref]; - if (!doc) { - console.warn(`Document not found: ${result.ref}`); - return null; - } - - // Calculate additional relevance boost for title matches - const titleBoost = this.calculateTitleBoost(doc, queryLower, queryTerms); - const keywordBoost = this.calculateKeywordBoost(doc, queryTerms); - const descriptionBoost = this.calculateDescriptionBoost(doc, queryTerms); - - // Apply boosts to base score - const enhancedScore = result.score * (1 + titleBoost + keywordBoost + descriptionBoost); - - return { - ...doc, - score: enhancedScore, - baseScore: result.score, - titleBoost, - keywordBoost, - descriptionBoost, - matchedTerms: Object.keys(result.matchData?.metadata || {}), - matchData: result.matchData, - strategy: result.strategy - }; - }).filter(Boolean); // Remove null results - } - - /** - * Calculate boost for title matches - * Heavily rewards exact and partial title matches - */ - calculateTitleBoost(doc, queryLower, queryTerms) { - if (!doc.title) return 0; - - const titleLower = doc.title.toLowerCase(); - let boost = 0; - - // Exact title match (highest boost) - if (titleLower === queryLower) { - boost += 10; - } - // Title starts with query - else if (titleLower.startsWith(queryLower)) { - boost += 8; - } - // Query is a significant part of title (e.g., "audit" in "Documentation Audit Guide") - else if (titleLower.includes(queryLower)) { - // Boost more if query is a larger portion of the title - const ratio = queryLower.length / titleLower.length; - boost += 5 * ratio + 3; - } - // All query terms appear in title - else if (queryTerms.every(term => titleLower.includes(term))) { - boost += 4; - } - // Some query terms appear in title - else { - const matchingTerms = queryTerms.filter(term => titleLower.includes(term)); - if (matchingTerms.length > 0) { - boost += 2 * (matchingTerms.length / queryTerms.length); - } - } - - // Additional boost if title contains query as a distinct word - const titleWords = titleLower.split(/[\s\-_:]+/); - if (titleWords.some(word => word === queryLower || word.startsWith(queryLower))) { - boost += 2; - } - - return boost; - } - - /** - * Calculate boost for keyword matches - */ - calculateKeywordBoost(doc, queryTerms) { - if (!doc.keywords) return 0; - - const keywords = Array.isArray(doc.keywords) - ? doc.keywords.map(k => k.toLowerCase()) - : doc.keywords.toLowerCase().split(/[\s,]+/); - - let boost = 0; - - queryTerms.forEach(term => { - if (keywords.some(kw => kw === term || kw.startsWith(term))) { - boost += 1.5; - } - }); - - return boost; - } - - /** - * Calculate boost for description matches - */ - calculateDescriptionBoost(doc, queryTerms) { - if (!doc.description) return 0; - - const descLower = doc.description.toLowerCase(); - let boost = 0; - - // Check if query terms appear early in description - queryTerms.forEach(term => { - const pos = descLower.indexOf(term); - if (pos !== -1) { - // Boost more if term appears early - boost += pos < 50 ? 1 : 0.5; - } - }); - - return boost; - } - - /** - * Group results by document and find matching sections - */ - groupResultsByDocument(results, query) { - const grouped = new Map(); - - results.forEach(result => { - const docId = result.id; - - if (!grouped.has(docId)) { - // Find matching sections within this document - const matchingSections = this.findMatchingSections(result, query); - - grouped.set(docId, { - ...result, - matchingSections, - totalMatches: 1, - combinedScore: result.score - }); - } else { - // Document already exists, combine scores and sections - const existing = grouped.get(docId); - const additionalSections = this.findMatchingSections(result, query); - - existing.matchingSections = this.mergeSections(existing.matchingSections, additionalSections); - existing.totalMatches += 1; - existing.combinedScore = Math.max(existing.combinedScore, result.score); - } - }); - - // Convert map to array and sort by combined score - return Array.from(grouped.values()) - .sort((a, b) => b.combinedScore - a.combinedScore); - } - - /** - * Find matching sections within a document - */ - findMatchingSections(result, query) { - const matchingSections = []; - const queryTerms = query.toLowerCase().split(/\s+/); - - // Check if title matches - if (result.title) { - const titleText = result.title.toLowerCase(); - const hasMatch = queryTerms.some(term => titleText.includes(term)); - - if (hasMatch) { - matchingSections.push({ - type: 'title', - text: result.title, - level: 1, - anchor: '' - }); - } - } - - // Check headings for matches - if (result.headings && Array.isArray(result.headings)) { - result.headings.forEach(heading => { - const headingText = heading.text?.toLowerCase() || ''; - const hasMatch = queryTerms.some(term => headingText.includes(term)); - - if (hasMatch) { - matchingSections.push({ - type: 'heading', - text: heading.text, - level: heading.level || 2, - anchor: this.generateAnchor(heading.text) - }); - } - }); - } - - // If no specific sections found, add a general content match - if (matchingSections.length === 0) { - matchingSections.push({ - type: 'content', - text: 'Content match', - level: 0, - anchor: '' - }); - } - - return matchingSections; - } - - /** - * Generate anchor link similar to how Sphinx does it - */ - generateAnchor(headingText) { - if (!headingText) return ''; - - return headingText - .toLowerCase() - .replace(/[^\w\s-]/g, '') // Remove special chars - .replace(/\s+/g, '-') // Replace spaces with hyphens - .trim(); - } - - /** - * Merge sections, avoiding duplicates - */ - mergeSections(existing, additional) { - const merged = [...existing]; - - additional.forEach(section => { - const isDuplicate = existing.some(existingSection => - existingSection.text === section.text && - existingSection.type === section.type - ); - - if (!isDuplicate) { - merged.push(section); - } - }); - - return merged; - } - - /** - * Get search statistics - */ - getStatistics() { - // Count facet keys and total values - const facetStats = {}; - Object.entries(this.facets).forEach(([key, valueSet]) => { - facetStats[key] = valueSet.size; - }); - - return { - documentsIndexed: Object.keys(this.documents).length, - topicsAvailable: this.topics.size, - tagsAvailable: this.tags.size, - documentTypesAvailable: this.documentTypes.size, - audienceAvailable: this.audience.size, - difficultiesAvailable: this.difficulties.size, - facetsDiscovered: Object.keys(this.facets).length, - facetStats: facetStats, - isInitialized: this.isInitialized - }; - } - - /** - * Check if the search engine is ready - */ - isReady() { - return this.isInitialized && this.index !== null; - } -} - -// Make SearchEngine available globally -window.SearchEngine = SearchEngine; diff --git a/docs/_ext/search_assets/modules/SearchInterface.js b/docs/_ext/search_assets/modules/SearchInterface.js deleted file mode 100644 index a0d793cbd..000000000 --- a/docs/_ext/search_assets/modules/SearchInterface.js +++ /dev/null @@ -1,615 +0,0 @@ -/** - * SearchInterface Module - * Handles the creation and management of the search UI - */ - -class SearchInterface { - constructor(options) { - this.options = options; - this.isVisible = false; - this.modal = null; - this.input = null; - this.resultsContainer = null; - this.statsContainer = null; - } - - /** - * Create the search interface elements - */ - create() { - // Check if we're on the search page - if (this.isSearchPage()) { - this.enhanceSearchPage(); - } else { - // On other pages, create the modal for search functionality - this.createModal(); - this.enhanceSearchButton(); - } - console.log('✅ Search interface created'); - } - - /** - * Check if we're on the search page - */ - isSearchPage() { - return window.location.pathname.includes('/search') || - window.location.pathname.includes('/search.html') || - window.location.pathname.endsWith('search/') || - document.querySelector('#search-results') !== null || - document.querySelector('.search-page') !== null || - document.querySelector('form[action*="search"]') !== null || - document.title.toLowerCase().includes('search') || - document.querySelector('h1')?.textContent.toLowerCase().includes('search'); - } - - /** - * Enhance the existing search page using the template structure - */ - enhanceSearchPage() { - console.log('🔍 Enhancing search page using existing template...'); - console.log('📄 Page URL:', window.location.href); - console.log('📋 Page title:', document.title); - - // Use the template's existing elements - this.input = document.querySelector('#enhanced-search-page-input'); - this.resultsContainer = document.querySelector('#enhanced-search-page-results'); - - console.log('🔎 Template search input found:', !!this.input); - console.log('📦 Template results container found:', !!this.resultsContainer); - - if (this.input && this.resultsContainer) { - console.log('✅ Using existing template structure - no additional setup needed'); - // The template's JavaScript will handle everything - return; - } - - // Fallback for non-template pages - console.log('⚠️ Template elements not found, falling back to generic search page detection'); - this.fallbackToGenericSearchPage(); - } - - /** - * Fallback for pages that don't use the template - */ - fallbackToGenericSearchPage() { - // Find existing search elements on generic pages - this.input = document.querySelector('#searchbox input[type="text"]') || - document.querySelector('input[name="q"]') || - document.querySelector('.search input[type="text"]'); - - // Find or create results container - this.resultsContainer = document.querySelector('#search-results') || - document.querySelector('.search-results') || - this.createResultsContainer(); - - // Create stats container - this.statsContainer = this.createStatsContainer(); - - // Hide default Sphinx search results if they exist - this.hideDefaultResults(); - - // Initialize with empty state - this.showEmptyState(); - - console.log('✅ Generic search page enhanced'); - } - - /** - * Create results container if it doesn't exist - */ - createResultsContainer() { - const container = document.createElement('div'); - container.id = 'enhanced-search-results'; - container.className = 'enhanced-search-results'; - - // Add basic styling to ensure proper positioning - container.style.cssText = ` - width: 100%; - max-width: none; - margin: 1rem 0; - clear: both; - position: relative; - z-index: 1; - `; - - // Find the best place to insert it within the main content area - const insertLocation = this.findBestInsertLocation(); - - if (insertLocation.parent && insertLocation.method === 'append') { - insertLocation.parent.appendChild(container); - console.log(`✅ Results container added to: ${insertLocation.parent.className || insertLocation.parent.tagName}`); - } else if (insertLocation.parent && insertLocation.method === 'after') { - insertLocation.parent.insertAdjacentElement('afterend', container); - console.log(`✅ Results container added after: ${insertLocation.parent.className || insertLocation.parent.tagName}`); - } else { - // Last resort - create a wrapper in main content - this.createInMainContent(container); - } - - return container; - } - - /** - * Find the best location to insert search results - */ - findBestInsertLocation() { - // Try to find existing search-related elements first - let searchResults = document.querySelector('.search-results, #search-results'); - if (searchResults) { - return { parent: searchResults, method: 'append' }; - } - - // Look for search form and place results after it - let searchForm = document.querySelector('#searchbox, .search form, form[action*="search"]'); - if (searchForm) { - return { parent: searchForm, method: 'after' }; - } - - // Look for main content containers (common Sphinx/theme classes) - const mainSelectors = [ - '.document .body', - '.document .documentwrapper', - '.content', - '.main-content', - '.page-content', - 'main', - '.container .row .col', - '.rst-content', - '.body-content' - ]; - - for (const selector of mainSelectors) { - const element = document.querySelector(selector); - if (element) { - return { parent: element, method: 'append' }; - } - } - - // Try to find any container that's not the body - const anyContainer = document.querySelector('.container, .wrapper, .page, #content'); - if (anyContainer) { - return { parent: anyContainer, method: 'append' }; - } - - return { parent: null, method: null }; - } - - /** - * Create container in main content as last resort - */ - createInMainContent(container) { - // Create a wrapper section - const wrapper = document.createElement('section'); - wrapper.className = 'search-page-content'; - wrapper.style.cssText = ` - max-width: 800px; - margin: 2rem auto; - padding: 0 1rem; - `; - - // Add a title - const title = document.createElement('h1'); - title.textContent = 'Search Results'; - title.style.cssText = 'margin-bottom: 1rem;'; - wrapper.appendChild(title); - - // Add the container - wrapper.appendChild(container); - - // Insert into body, but with proper styling - document.body.appendChild(wrapper); - - console.log('⚠️ Created search results in body with wrapper - consider improving page structure'); - } - - /** - * Create stats container - */ - createStatsContainer() { - const container = document.createElement('div'); - container.className = 'enhanced-search-stats'; - container.style.cssText = 'margin: 1rem 0; font-size: 0.9rem; color: #666;'; - - // Insert before results - if (this.resultsContainer && this.resultsContainer.parentNode) { - this.resultsContainer.parentNode.insertBefore(container, this.resultsContainer); - } - - return container; - } - - /** - * Hide default Sphinx search results - */ - hideDefaultResults() { - // Hide default search results that Sphinx might show - const defaultResults = document.querySelectorAll( - '.search-summary, .search li, #search-results .search, .searchresults' - ); - defaultResults.forEach(el => { - el.style.display = 'none'; - }); - } - - /** - * Create the main search modal (legacy - kept for compatibility) - */ - createModal() { - // Enhanced search modal - const modal = document.createElement('div'); - modal.id = 'enhanced-search-modal'; - modal.className = 'enhanced-search-modal'; - modal.innerHTML = ` -
-
-
-
- - - -
-
-
-
- -
- `; - - document.body.appendChild(modal); - - // Cache references - this.modal = modal; - this.input = modal.querySelector('#enhanced-search-input'); - this.resultsContainer = modal.querySelector('.enhanced-search-results'); - this.statsContainer = modal.querySelector('.enhanced-search-stats'); - - // Add event handlers for closing the modal - const closeButton = modal.querySelector('.enhanced-search-close'); - const backdrop = modal.querySelector('.enhanced-search-backdrop'); - - if (closeButton) { - closeButton.addEventListener('click', () => this.hideModal()); - } - - if (backdrop) { - backdrop.addEventListener('click', () => this.hideModal()); - } - - // Hide modal by default - modal.style.display = 'none'; - - // Initialize with empty state - this.showEmptyState(); - } - - /** - * Replace or enhance existing search button to show modal - */ - enhanceSearchButton() { - // Find existing search button/form - const searchForm = document.querySelector('#searchbox form') || - document.querySelector('.search form') || - document.querySelector('form[action*="search"]'); - - if (searchForm) { - // Prevent form submission and show modal instead - searchForm.addEventListener('submit', (e) => { - e.preventDefault(); - this.showModal(); - }); - console.log('✅ Search form enhanced to show modal'); - } - - // Find search button specifically and enhance it - const existingButton = document.querySelector('.search-button-field, .search-button__button'); - if (existingButton) { - existingButton.addEventListener('click', (e) => { - e.preventDefault(); - this.showModal(); - }); - console.log('✅ Search button enhanced to show modal'); - } - - // Also look for search input fields and enhance them - const searchInput = document.querySelector('#searchbox input[type="text"]') || - document.querySelector('.search input[type="text"]'); - if (searchInput) { - searchInput.addEventListener('focus', () => { - this.showModal(); - }); - console.log('✅ Search input enhanced to show modal on focus'); - } - } - - /** - * Show the search interface (focus input or show modal) - */ - show() { - if (this.modal) { - this.showModal(); - } else if (this.input) { - this.input.focus(); - this.input.select(); - } - } - - /** - * Hide the search interface (hide modal or blur input) - */ - hide() { - if (this.modal) { - this.hideModal(); - } else if (this.input) { - this.input.blur(); - } - } - - /** - * Show the modal - */ - showModal() { - if (this.modal) { - this.modal.style.display = 'flex'; - this.modal.classList.add('visible'); - this.isVisible = true; - // Focus the input after a brief delay to ensure modal is visible - setTimeout(() => { - if (this.input) { - this.input.focus(); - this.input.select(); - } - }, 100); - console.log('🔍 Search modal shown'); - } - } - - /** - * Hide the modal - */ - hideModal() { - if (this.modal) { - this.modal.classList.remove('visible'); - this.isVisible = false; - // Hide after animation completes - setTimeout(() => { - if (this.modal) { - this.modal.style.display = 'none'; - } - }, 200); - // Clear any search results - this.showEmptyState(); - console.log('🔍 Search modal hidden'); - } - } - - /** - * Get the search input element - */ - getInput() { - return this.input; - } - - /** - * Get the results container - */ - getResultsContainer() { - return this.resultsContainer; - } - - /** - * Get the stats container - */ - getStatsContainer() { - return this.statsContainer; - } - - /** - * Get the modal element - */ - getModal() { - return this.modal; - } - - /** - * Check if modal is visible - */ - isModalVisible() { - return this.isVisible && this.modal && this.modal.style.display !== 'none'; - } - - /** - * Show empty state in results - */ - showEmptyState() { - if (this.resultsContainer) { - this.resultsContainer.innerHTML = ` -
- -

Start typing to search documentation...

-
- Search tips: -
    -
  • Use specific terms for better results
  • -
  • Try different keywords if you don't find what you're looking for
  • -
  • Search includes titles, content, headings, and tags
  • -
-
-
- `; - } - } - - /** - * Show no results state - */ - showNoResults(query) { - if (this.resultsContainer) { - this.resultsContainer.innerHTML = ` -
- -

No results found for "${this.escapeHtml(query)}"

-
- Try: -
    -
  • Checking for typos
  • -
  • Using different or more general terms
  • -
  • Using fewer keywords
  • -
-
-
- `; - } - } - - /** - * Show error state - */ - showError(message = 'Search temporarily unavailable') { - if (this.resultsContainer) { - this.resultsContainer.innerHTML = ` -
- -

${this.escapeHtml(message)}

-
- `; - } - } - - /** - * Update search statistics - */ - updateStats(query, count) { - if (this.statsContainer) { - if (count > 0) { - this.statsContainer.innerHTML = `${count} result${count !== 1 ? 's' : ''} for "${this.escapeHtml(query)}"`; - } else { - this.statsContainer.innerHTML = `No results for "${this.escapeHtml(query)}"`; - } - } - } - - /** - * Clear search statistics - */ - clearStats() { - if (this.statsContainer) { - this.statsContainer.innerHTML = ''; - } - } - - /** - * Get current search query - */ - getQuery() { - return this.input ? this.input.value.trim() : ''; - } - - /** - * Set search query - */ - setQuery(query) { - if (this.input) { - this.input.value = query; - } - } - - /** - * Clear search query - */ - clearQuery() { - if (this.input) { - this.input.value = ''; - } - } - - /** - * Focus the search input - */ - focusInput() { - if (this.input) { - this.input.focus(); - } - } - - /** - * Get close button for event binding - */ - getCloseButton() { - return this.modal ? this.modal.querySelector('.enhanced-search-close') : null; - } - - /** - * Get backdrop for event binding - */ - getBackdrop() { - return this.modal ? this.modal.querySelector('.enhanced-search-backdrop') : null; - } - - /** - * Escape HTML to prevent XSS - */ - escapeHtml(unsafe) { - return unsafe - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - } - - /** - * Add CSS class to modal - */ - addModalClass(className) { - if (this.modal) { - this.modal.classList.add(className); - } - } - - /** - * Remove CSS class from modal - */ - removeModalClass(className) { - if (this.modal) { - this.modal.classList.remove(className); - } - } - - /** - * Check if modal has class - */ - hasModalClass(className) { - return this.modal ? this.modal.classList.contains(className) : false; - } - - /** - * Destroy the search interface - */ - destroy() { - if (this.modal) { - this.modal.remove(); - this.modal = null; - this.input = null; - this.resultsContainer = null; - this.statsContainer = null; - } - this.isVisible = false; - } -} - -// Make SearchInterface available globally -window.SearchInterface = SearchInterface; diff --git a/docs/_ext/search_assets/modules/SearchPageManager.js b/docs/_ext/search_assets/modules/SearchPageManager.js deleted file mode 100644 index 7225f0fba..000000000 --- a/docs/_ext/search_assets/modules/SearchPageManager.js +++ /dev/null @@ -1,1204 +0,0 @@ -/** - * Search Page Manager Module - * Handles search functionality on the dedicated search page with filtering and grouping - */ - -class SearchPageManager { - constructor() { - this.searchInput = null; - this.resultsContainer = null; - this.searchEngine = null; - this.documents = []; - this.currentQuery = ''; - this.allResults = []; - this.currentFilters = { - topic: '', - category: '', // Legacy alias - tag: '', - type: '', - facets: {} // Dynamic facets - }; - this.filterOptions = { - topics: [], - categories: [], // Legacy alias - tags: [], - documentTypes: [], - audience: [], - personas: [], // Legacy alias - difficulties: [], - facets: {} // Dynamic facets - }; - - this.init(); - } - - async init() { - console.log('🔍 Initializing search page...'); - - // Get page elements - this.searchInput = document.querySelector('#enhanced-search-page-input'); - this.resultsContainer = document.querySelector('#enhanced-search-page-results'); - - if (!this.searchInput || !this.resultsContainer) { - console.error('❌ Required search page elements not found'); - return; - } - - // Wait for enhanced search to be available - await this.waitForEnhancedSearch(); - - // Create filter interface - this.createFilterInterface(); - - // Set up event listeners - this.setupEventListeners(); - - // Handle URL search parameter - this.handleUrlSearch(); - - console.log('✅ Search page initialized'); - } - - async waitForEnhancedSearch() { - return new Promise((resolve) => { - const checkForSearch = () => { - if (window.enhancedSearchInstance && window.enhancedSearchInstance.isLoaded) { - this.searchEngine = window.enhancedSearchInstance.getSearchEngine(); - this.documents = window.enhancedSearchInstance.getDocuments(); - - // Get filter options - if (this.searchEngine && this.searchEngine.getFilterOptions) { - this.filterOptions = this.searchEngine.getFilterOptions(); - console.log('✅ Filter options loaded:', this.filterOptions); - } - - resolve(); - } else { - setTimeout(checkForSearch, 100); - } - }; - checkForSearch(); - }); - } - - createFilterInterface() { - // Get the search controls container - const searchControlsContainer = this.searchInput.parentNode; - - // Add unified styling to the container - searchControlsContainer.className = 'search-controls-container mb-4'; - - // Create filter section - const filterSection = document.createElement('div'); - filterSection.className = 'search-filters'; - filterSection.innerHTML = this.renderFilterInterface(); - - // Wrap the search input in a styled container - const searchInputWrapper = document.createElement('div'); - searchInputWrapper.className = 'search-input-wrapper'; - searchInputWrapper.innerHTML = ` - - `; - this.searchInput.parentNode.insertBefore(searchInputWrapper, this.searchInput); - searchInputWrapper.appendChild(this.searchInput); - - // Insert filters before the search input wrapper within the same container - searchControlsContainer.insertBefore(filterSection, searchInputWrapper); - - // Add search input wrapper class for consistent styling - this.searchInput.className = 'search-input-field'; - this.searchInput.placeholder = 'Search documentation...'; - - // Bind filter events - this.bindFilterEvents(); - } - - renderFilterInterface() { - // Use topics (new) or categories (legacy) with null safety - const topics = this.filterOptions.topics || this.filterOptions.categories || []; - const topicOptions = topics.map(topic => - `` - ).join(''); - - const tags = this.filterOptions.tags || []; - const tagOptions = tags.map(tag => - `` - ).join(''); - - const types = this.filterOptions.documentTypes || []; - const typeOptions = types.map(type => - `` - ).join(''); - - // Use audience (new) or personas (legacy) with null safety - const audience = this.filterOptions.audience || this.filterOptions.personas || []; - const audienceOptions = audience.map(aud => - `` - ).join(''); - - const difficulties = this.filterOptions.difficulties || []; - const difficultyOptions = difficulties.map(difficulty => - `` - ).join(''); - - // Dynamic facets - render additional filter dropdowns for each facet - const facetFilters = this.renderDynamicFacetFilters(); - - // Count active filters - const activeCount = this.getActiveFilterCount(); - - return ` -
-
- - Filters - ${activeCount > 0 ? `${activeCount}` : ''} -
- -
-
-
- -
- - -
-
- -
- -
- - -
-
- -
- -
- - -
-
- - ${facetFilters} -
- `; - } - - getActiveFilterCount() { - let count = 0; - if (this.currentFilters.topic || this.currentFilters.category) count++; - if (this.currentFilters.tag) count++; - if (this.currentFilters.type) count++; - if (this.currentFilters.facets) { - Object.values(this.currentFilters.facets).forEach(v => { if (v) count++; }); - } - return count; - } - - updateFilterUI() { - const activeCount = this.getActiveFilterCount(); - const countBadge = document.querySelector('.active-filter-count'); - const clearBtn = document.getElementById('clear-filters'); - - if (countBadge) { - if (activeCount > 0) { - countBadge.textContent = activeCount; - countBadge.style.display = 'inline-flex'; - } else { - countBadge.style.display = 'none'; - } - } - - if (clearBtn) { - clearBtn.classList.toggle('hidden', activeCount === 0); - } - - // Update select wrappers with active state - document.querySelectorAll('.filter-select').forEach(select => { - const wrapper = select.closest('.filter-select-wrapper'); - if (wrapper) { - wrapper.classList.toggle('has-value', select.value !== ''); - } - }); - } - - renderDynamicFacetFilters() { - const facets = this.filterOptions.facets || {}; - - return Object.entries(facets).map(([facetKey, facetValues]) => { - if (!Array.isArray(facetValues) || facetValues.length === 0) return ''; - - const options = facetValues.map(value => - `` - ).join(''); - - const icon = this.getFacetIcon(facetKey); - - return ` -
- -
- - -
-
- `; - }).join(''); - } - - getFacetIcon(facetKey) { - const iconMap = { - 'modality': 'fa-solid fa-layer-group', - 'framework': 'fa-solid fa-cube', - 'platform': 'fa-solid fa-desktop', - 'language': 'fa-solid fa-code', - 'version': 'fa-solid fa-code-branch', - 'status': 'fa-solid fa-circle-check' - }; - return iconMap[facetKey.toLowerCase()] || 'fa-solid fa-filter'; - } - - formatFacetName(facetKey) { - return facetKey - .split(/[-_]/) - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - } - - formatFacetValue(value) { - if (typeof value !== 'string') return String(value); - return value - .split(/[-_]/) - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - } - - formatCategoryName(category) { - return category - .split(/[-_]/) - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - } - - formatTypeName(type) { - return type - .split(/[-_]/) - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - } - - formatPersonaName(persona) { - // Convert "data-scientist-focused" to "Data Scientist Focused" - return persona - .replace(/-focused$/, '') // Remove "-focused" suffix - .split(/[-_]/) - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - } - - formatDifficultyName(difficulty) { - return difficulty.charAt(0).toUpperCase() + difficulty.slice(1); - } - - formatModalityName(modality) { - return modality - .split(/[-_]/) - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - } - - bindFilterEvents() { - // Topic filter (new schema, replaces category) - const topicFilter = document.getElementById('topic-filter'); - if (topicFilter) { - topicFilter.addEventListener('change', (e) => { - this.currentFilters.topic = e.target.value; - this.currentFilters.category = e.target.value; // Legacy alias - this.updateFilterUI(); - this.applyFiltersAndSearch(); - }); - } - - // Tag filter - const tagFilter = document.getElementById('tag-filter'); - if (tagFilter) { - tagFilter.addEventListener('change', (e) => { - this.currentFilters.tag = e.target.value; - this.updateFilterUI(); - this.applyFiltersAndSearch(); - }); - } - - // Type filter - const typeFilter = document.getElementById('type-filter'); - if (typeFilter) { - typeFilter.addEventListener('change', (e) => { - this.currentFilters.type = e.target.value; - this.updateFilterUI(); - this.applyFiltersAndSearch(); - }); - } - - // Dynamic facet filters - document.querySelectorAll('.facet-filter').forEach(select => { - select.addEventListener('change', (e) => { - const facetKey = e.target.dataset.facetKey; - if (!this.currentFilters.facets) { - this.currentFilters.facets = {}; - } - this.currentFilters.facets[facetKey] = e.target.value; - // Also set flat key for backwards compatibility - this.currentFilters[facetKey] = e.target.value; - this.updateFilterUI(); - this.applyFiltersAndSearch(); - }); - }); - - // Clear filters - const clearBtn = document.getElementById('clear-filters'); - if (clearBtn) { - clearBtn.addEventListener('click', () => { - this.clearFilters(); - }); - } - } - - clearFilters() { - this.currentFilters = { - topic: '', - category: '', // Legacy alias - tag: '', - type: '', - facets: {} - }; - - // Reset filter selects with null safety - const topicFilter = document.getElementById('topic-filter'); - if (topicFilter) topicFilter.value = ''; - - const tagFilter = document.getElementById('tag-filter'); - if (tagFilter) tagFilter.value = ''; - - const typeFilter = document.getElementById('type-filter'); - if (typeFilter) typeFilter.value = ''; - - // Reset dynamic facet filters - document.querySelectorAll('.facet-filter').forEach(select => { - select.value = ''; - }); - - // Update filter UI state - this.updateFilterUI(); - - // Clear active filter display - this.updateActiveFiltersDisplay(); - - // Re-run search - this.applyFiltersAndSearch(); - } - - handleBadgeClick(filterType, filterValue) { - // Handle dynamic facet filters (facet-modality, facet-framework, etc.) - if (filterType.startsWith('facet-')) { - const facetKey = filterType.replace('facet-', ''); - if (!this.currentFilters.facets) { - this.currentFilters.facets = {}; - } - this.currentFilters.facets[facetKey] = filterValue; - this.currentFilters[facetKey] = filterValue; // Flat alias - - // Update dropdown if it exists - const dropdown = document.getElementById(`facet-${facetKey}-filter`); - if (dropdown) { - dropdown.value = filterValue; - } - } else { - // Standard filter - this.currentFilters[filterType] = filterValue; - - // Handle legacy aliases and update corresponding dropdowns - if (filterType === 'topic') { - this.currentFilters.category = filterValue; - const topicDropdown = document.getElementById('topic-filter'); - if (topicDropdown) topicDropdown.value = filterValue; - } else if (filterType === 'audience') { - this.currentFilters.persona = filterValue; - const audienceDropdown = document.getElementById('audience-filter'); - if (audienceDropdown) audienceDropdown.value = filterValue; - } else if (filterType === 'difficulty') { - const difficultyDropdown = document.getElementById('difficulty-filter'); - if (difficultyDropdown) difficultyDropdown.value = filterValue; - } else if (filterType === 'tag') { - const tagDropdown = document.getElementById('tag-filter'); - if (tagDropdown) tagDropdown.value = filterValue; - } else if (filterType === 'type') { - const typeDropdown = document.getElementById('type-filter'); - if (typeDropdown) typeDropdown.value = filterValue; - } else { - // Fallback: try to update dropdown by filter type - const dropdown = document.getElementById(`${filterType}-filter`); - if (dropdown) { - dropdown.value = filterValue; - } - } - } - - // Update active filters display - this.updateActiveFiltersDisplay(); - - // Re-run search - this.applyFiltersAndSearch(); - } - - updateActiveFiltersDisplay() { - // Remove existing active filters display - const existingDisplay = document.querySelector('.active-filters-display'); - if (existingDisplay) { - existingDisplay.remove(); - } - - // Check for active dynamic facet filters (not in standard dropdowns) - const activeMetadataFilters = []; - - // Dynamic facet filters - if (this.currentFilters.facets) { - Object.entries(this.currentFilters.facets).forEach(([facetKey, facetValue]) => { - if (facetValue) { - activeMetadataFilters.push(`🏷️ ${this.formatFacetName(facetKey)}: ${this.formatFacetValue(facetValue)}`); - } - }); - } - - if (activeMetadataFilters.length > 0) { - const filtersContainer = document.querySelector('.search-filters'); - if (filtersContainer) { - const activeFiltersHtml = ` -
- Active filters: - ${activeMetadataFilters.map(filter => `${filter}`).join(' ')} - -
- `; - filtersContainer.insertAdjacentHTML('afterend', activeFiltersHtml); - } - } - } - - clearMetadataFilters() { - this.currentFilters.facets = {}; - - // Reset dynamic facet filters in UI - document.querySelectorAll('.facet-filter').forEach(select => { - select.value = ''; - }); - - this.updateActiveFiltersDisplay(); - this.applyFiltersAndSearch(); - } - - applyFiltersAndSearch() { - if (this.currentQuery) { - this.handleSearch(this.currentQuery); - } - } - - setupEventListeners() { - // Search input - this.searchInput.addEventListener('input', this.debounce((e) => { - this.handleSearch(e.target.value); - }, 300)); - - this.searchInput.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - this.handleSearch(e.target.value); - } - }); - - // Badge click handlers (using event delegation) - this.resultsContainer.addEventListener('click', (e) => { - if (e.target.classList.contains('clickable-badge')) { - const filterType = e.target.dataset.filterType; - const filterValue = e.target.dataset.filterValue; - this.handleBadgeClick(filterType, filterValue); - } - }); - - // Make instance available globally for button callbacks - window.searchPageManager = this; - - // Initialize keyboard navigation state - this.focusedIndex = -1; - - // Focus input on page load - this.searchInput.focus(); - } - - /** - * Setup keyboard navigation for search results - */ - setupKeyboardNavigation() { - // Reset focused index when results change - this.focusedIndex = -1; - - // Use a single event listener on the document (avoiding duplicates) - if (!this.keyboardNavigationInitialized) { - this.keyboardNavigationInitialized = true; - - document.addEventListener('keydown', (e) => { - const results = this.resultsContainer.querySelectorAll('.search-result'); - if (results.length === 0) return; - - // Only handle when search area is focused - if (!this.isSearchFocused()) return; - - if (e.key === 'ArrowDown') { - e.preventDefault(); - this.focusedIndex = Math.min(this.focusedIndex + 1, results.length - 1); - this.focusResult(results, this.focusedIndex); - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - this.focusedIndex = Math.max(this.focusedIndex - 1, 0); - this.focusResult(results, this.focusedIndex); - } else if (e.key === 'Enter' && this.focusedIndex >= 0) { - e.preventDefault(); - const link = results[this.focusedIndex].querySelector('a'); - if (link) link.click(); - } else if (e.key === 'Escape') { - this.focusedIndex = -1; - this.clearFocus(); - this.searchInput.focus(); - } - }); - } - } - - /** - * Check if search area is focused - */ - isSearchFocused() { - const active = document.activeElement; - return active === this.searchInput || - this.resultsContainer.contains(active) || - this.resultsContainer.querySelector('.focused'); - } - - /** - * Focus a specific result by index - */ - focusResult(results, index) { - this.clearFocus(); - const element = results[index]; - if (element) { - element.classList.add('focused'); - element.setAttribute('aria-selected', 'true'); - element.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - } - } - - /** - * Clear focus from all results - */ - clearFocus() { - this.resultsContainer.querySelectorAll('.search-result.focused') - .forEach(el => { - el.classList.remove('focused'); - el.setAttribute('aria-selected', 'false'); - }); - } - - handleUrlSearch() { - const urlParams = new URLSearchParams(window.location.search); - const query = urlParams.get('q'); - if (query) { - this.searchInput.value = query; - this.handleSearch(query); - } - } - - handleSearch(query) { - this.currentQuery = query.trim(); - - if (!this.currentQuery) { - this.showEmptyState(); - return; - } - - if (this.currentQuery.length < 2) { - this.showMinLengthMessage(); - return; - } - - // Perform search with filters - const results = this.searchEngine.search(this.currentQuery, this.currentFilters); - this.allResults = results; - this.displayResults(results); - - // Update URL without reload - const newUrl = new URL(window.location); - newUrl.searchParams.set('q', this.currentQuery); - window.history.replaceState(null, '', newUrl); - } - - displayResults(results) { - if (results.length === 0) { - this.showNoResults(); - return; - } - - const resultsHtml = results.map((result, index) => this.renderResult(result, index)).join(''); - const resultBreakdown = this.getResultBreakdown(results); - - this.resultsContainer.innerHTML = ` - -
-

Search Results

-

- Found ${results.length} result${results.length !== 1 ? 's' : ''} for "${this.escapeHtml(this.currentQuery)}" - ${this.getActiveFiltersText()} - ${resultBreakdown ? `${resultBreakdown}` : ''} -

-
-
- ${resultsHtml} -
- `; - - // Setup keyboard navigation - this.setupKeyboardNavigation(); - - // Emit event for AI assistant integration - this.emitSearchAIRequest(this.currentQuery, results); - } - - /** - * Get result type breakdown for display - */ - getResultBreakdown(results) { - const byType = {}; - results.forEach(r => { - const type = r.content_type || 'Other'; - byType[type] = (byType[type] || 0) + 1; - }); - - const breakdown = Object.entries(byType) - .sort((a, b) => b[1] - a[1]) - .map(([type, count]) => `${count} ${this.escapeHtml(this.formatTypeName(type))}`) - .join(' · '); - - return breakdown; - } - - /** - * Render topic badges for a result - */ - renderTopicBadges(result) { - const topics = this.searchEngine.getDocumentTopics - ? this.searchEngine.getDocumentTopics(result) - : []; - - if (!topics || topics.length === 0) return ''; - - const topicBadges = topics.slice(0, 3).map(topic => - ` - 📁 ${this.escapeHtml(topic)} - ` - ).join(''); - - const moreBadge = topics.length > 3 - ? `+${topics.length - 3}` - : ''; - - return `
${topicBadges}${moreBadge}
`; - } - - getActiveFiltersText() { - const activeFilters = []; - - // Topic (new) or category (legacy) - const topicFilter = this.currentFilters.topic || this.currentFilters.category; - if (topicFilter) { - activeFilters.push(`Topic: ${this.formatCategoryName(topicFilter)}`); - } - if (this.currentFilters.tag) { - activeFilters.push(`Tag: ${this.currentFilters.tag}`); - } - if (this.currentFilters.type) { - activeFilters.push(`Type: ${this.formatTypeName(this.currentFilters.type)}`); - } - - // Dynamic facets - if (this.currentFilters.facets) { - Object.entries(this.currentFilters.facets).forEach(([facetKey, facetValue]) => { - if (facetValue) { - activeFilters.push(`${this.formatFacetName(facetKey)}: ${this.formatFacetValue(facetValue)}`); - } - }); - } - - return activeFilters.length > 0 ? ` (filtered by ${activeFilters.join(', ')})` : ''; - } - - renderResult(result, index) { - const title = this.highlightText(result.title, this.currentQuery); - // Use description (frontmatter) > summary > generated snippet - const snippetSource = result.description || result.summary || this.generateSnippet(result.content, this.currentQuery, 200); - const summary = this.highlightText(snippetSource || '', this.currentQuery); - const breadcrumb = this.getBreadcrumb(result.id); - const sectionInfo = this.getSectionInfo(result.id); - const matchingSections = this.renderMatchingSections(result, this.currentQuery); - const resultTags = this.renderResultTags(result); - const topicBadges = this.renderTopicBadges(result); - const metadataBadges = this.renderMetadataBadges(result); - - // Multiple matches indicator - const multipleMatchesIndicator = result.totalMatches > 1 - ? `+${result.totalMatches - 1} more matches` - : ''; - - return ` -
-
-
- -
-
-

- ${title} - ${multipleMatchesIndicator} -

-
- ${breadcrumb} -
- ${topicBadges} -
- ${metadataBadges} -
- ${resultTags} -
-
-
-

${summary}

- ${matchingSections} -
-
- `; - } - - /** - * Generate context-aware snippet around search terms - */ - generateSnippet(content, query, maxLength = 200) { - if (!content) return ''; - - // Find first occurrence of any search term - const terms = query.toLowerCase().split(/\s+/).filter(t => t.length > 2); - const lowerContent = content.toLowerCase(); - - let startIndex = 0; - for (const term of terms) { - const idx = lowerContent.indexOf(term); - if (idx > 0) { - startIndex = Math.max(0, idx - 50); // Start 50 chars before match - break; - } - } - - // Extract snippet around match - let snippet = content.substring(startIndex, startIndex + maxLength); - - // Clean up word boundaries - if (startIndex > 0) { - const firstSpace = snippet.indexOf(' '); - if (firstSpace > 0 && firstSpace < 20) { - snippet = snippet.substring(firstSpace + 1); - } - snippet = '...' + snippet; - } - - if (startIndex + maxLength < content.length) { - const lastSpace = snippet.lastIndexOf(' '); - if (lastSpace > snippet.length - 20) { - snippet = snippet.substring(0, lastSpace); - } - snippet += '...'; - } - - return snippet; - } - - renderResultTags(result) { - const tags = this.searchEngine.getDocumentTags(result); - if (!tags || tags.length === 0) return ''; - - const tagsToShow = tags.slice(0, 6); // Show more tags since they're now on their own line - const tagsHtml = tagsToShow.map(tag => - `${tag}` - ).join(''); - - const moreText = tags.length > 6 ? `+${tags.length - 6} more` : ''; - - return `
${tagsHtml}${moreText}
`; - } - - renderResultCategories(result) { - // Use getDocumentTopics (new) which falls back to getDocumentCategories (legacy) - const topics = this.searchEngine.getDocumentTopics - ? this.searchEngine.getDocumentTopics(result) - : this.searchEngine.getDocumentCategories(result); - if (!topics || topics.length === 0) return ''; - - const topicsHtml = topics.slice(0, 2).map(topic => - `${this.formatCategoryName(topic)}` - ).join(''); - - return `
${topicsHtml}
`; - } - - renderMetadataBadges(result) { - const badges = []; - - // Audience badges (new) or personas (legacy) - render each as separate badge - const audienceField = result.audience || result.personas; - if (audienceField) { - // Parse audience list - handle array, comma-separated string, or space-separated string with known patterns - let audienceList = []; - if (Array.isArray(audienceField)) { - audienceList = audienceField; - } else if (typeof audienceField === 'string') { - // Check for comma separation first - if (audienceField.includes(',')) { - audienceList = audienceField.split(',').map(a => a.trim()).filter(Boolean); - } else { - // Try to match known audience patterns (e.g., "Technical Writer Developer" -> ["Technical Writer", "Developer"]) - const knownAudiences = ['Technical Writer', 'Developer', 'Data Scientist', 'ML Engineer', 'DevOps', 'Administrator', 'Researcher']; - const matches = []; - let remaining = audienceField; - - for (const known of knownAudiences) { - if (remaining.includes(known)) { - matches.push(known); - remaining = remaining.replace(known, '').trim(); - } - } - - audienceList = matches.length > 0 ? matches : [audienceField]; - } - } - - audienceList.forEach(audience => { - const formatted = this.formatPersonaName(audience); - badges.push(``); - }); - } - - // Difficulty badge - if (result.difficulty) { - const difficultyIcon = this.getDifficultyIcon(result.difficulty); - badges.push(``); - } - - // Dynamic facet badges - if (result.facets && typeof result.facets === 'object') { - Object.entries(result.facets).forEach(([facetKey, facetValue]) => { - if (facetValue) { - const values = Array.isArray(facetValue) ? facetValue : [facetValue]; - values.forEach(value => { - badges.push(``); - }); - } - }); - } - - // Legacy flat modality badge (if not in facets) - if (result.modality && (!result.facets || !result.facets.modality)) { - const modalityIcon = this.getModalityIcon(result.modality); - badges.push(``); - } - - return badges.join(''); - } - - getDifficultyIcon(difficulty) { - switch (difficulty.toLowerCase()) { - case 'beginner': return '🔰'; - case 'intermediate': return '📊'; - case 'advanced': return '🚀'; - case 'reference': return '📚'; - default: return '📖'; - } - } - - getModalityIcon(modality) { - switch (modality.toLowerCase()) { - case 'text-only': return '📝'; - case 'image-only': return '🖼️'; - case 'video-only': return '🎥'; - case 'multimodal': return '🔀'; - case 'universal': return '🌐'; - default: return '📄'; - } - } - - renderMatchingSections(result, query) { - if (!result.matchingSections || result.matchingSections.length <= 1) { - return ''; - } - - const sectionsToShow = result.matchingSections.slice(0, 5); - const hasMore = result.matchingSections.length > 5; - - const sectionsHtml = sectionsToShow.map(section => { - const sectionIcon = this.getSectionIcon(section.type, section.level); - const sectionText = this.highlightText(section.text, query); - const anchor = section.anchor ? `#${section.anchor}` : ''; - const sectionUrl = this.getDocumentUrl(result) + anchor; - - return ` - - ${sectionIcon} - ${sectionText} - - - `; - }).join(''); - - const moreIndicator = hasMore ? ` -
- - +${result.matchingSections.length - 5} more sections -
- ` : ''; - - return ` -
-
- - Matching sections: -
- -
- `; - } - - getSectionIcon(type, level) { - switch (type) { - case 'title': - return ''; - case 'heading': - if (level <= 2) return ''; - if (level <= 4) return ''; - return ''; - case 'content': - return ''; - default: - return ''; - } - } - - getBreadcrumb(docId) { - const parts = docId.split('/').filter(part => part && part !== 'index'); - return parts.length > 0 ? parts.join(' › ') : 'Home'; - } - - getSectionInfo(docId) { - const path = docId.toLowerCase(); - - if (path.includes('get-started') || path.includes('getting-started')) { - return { - class: 'getting-started', - icon: 'fas fa-rocket', - label: 'Getting Started' - }; - } else if (path.includes('admin')) { - return { - class: 'admin', - icon: 'fas fa-cog', - label: 'Administration' - }; - } else if (path.includes('reference') || path.includes('api')) { - return { - class: 'reference', - icon: 'fas fa-book', - label: 'Reference' - }; - } else if (path.includes('about') || path.includes('concepts')) { - return { - class: 'about', - icon: 'fas fa-info-circle', - label: 'About' - }; - } else if (path.includes('tutorial')) { - return { - class: 'tutorial', - icon: 'fas fa-graduation-cap', - label: 'Tutorial' - }; - } else { - return { - class: 'default', - icon: 'fas fa-file-lines', - label: 'Documentation' - }; - } - } - - getDocumentUrl(result) { - if (result.url) { - return result.url; - } - return `${result.id.replace(/^\/+/, '')}.html`; - } - - highlightText(text, query) { - if (!query) return this.escapeHtml(text); - - const terms = query.toLowerCase().split(/\s+/).filter(term => term.length > 1); - let highlightedText = this.escapeHtml(text); - - terms.forEach(term => { - const regex = new RegExp(`(${this.escapeRegex(term)})`, 'gi'); - highlightedText = highlightedText.replace(regex, '$1'); - }); - - return highlightedText; - } - - showEmptyState() { - this.resultsContainer.innerHTML = ` -
- -

Search Documentation

-

Start typing to search across all documentation pages...

-
- - - Search Tips: Use specific terms for better results • Use filters to narrow down results • Search includes titles, content, and headings - -
-
- `; - } - - showMinLengthMessage() { - this.resultsContainer.innerHTML = ` -
- -

Keep typing...

-

Enter at least 2 characters to search

-
- `; - } - - showNoResults() { - const filtersActive = this.currentFilters.topic || this.currentFilters.category || - this.currentFilters.tag || this.currentFilters.type || - (this.currentFilters.facets && Object.keys(this.currentFilters.facets).some(k => this.currentFilters.facets[k])); - const suggestionText = filtersActive - ? 'Try clearing some filters or using different keywords' - : 'Try different keywords or check your spelling'; - - this.resultsContainer.innerHTML = ` -
- -

No results found

-

No results found for "${this.escapeHtml(this.currentQuery)}"${this.getActiveFiltersText()}

-
- - ${suggestionText} - -
- ${filtersActive ? ` -
- -
- ` : ''} -
- `; - } - - // Utility methods - debounce(func, wait) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; - } - - escapeHtml(unsafe) { - return unsafe - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - } - - escapeRegex(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - } - - emitSearchAIRequest(query, results) { - // Emit event for AI assistant integration (search page) - const aiRequestEvent = new CustomEvent('search-ai-request', { - detail: { - query: query, - results: results, - count: results.length, - container: 'ai-assistant-container' - } - }); - document.dispatchEvent(aiRequestEvent); - - console.log(`🤖 Emitted search-ai-request event for query: "${query}" with ${results.length} results`); - } -} diff --git a/docs/_ext/search_assets/modules/Utils.js b/docs/_ext/search_assets/modules/Utils.js deleted file mode 100644 index f93dc4dda..000000000 --- a/docs/_ext/search_assets/modules/Utils.js +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Utils Module - * Contains utility functions used across the enhanced search system - */ - -class Utils { - constructor() { - // Utility class - no initialization needed - } - - /** - * Debounce function to limit rapid function calls - */ - debounce(func, wait) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; - } - - /** - * Escape special regex characters - */ - escapeRegex(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - } - - /** - * Escape HTML to prevent XSS attacks - */ - escapeHtml(unsafe) { - return unsafe - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - } - - /** - * Highlight search terms in text - */ - highlightText(text, query, highlightClass = 'search-highlight') { - if (!query || !text) return text; - - const terms = query.toLowerCase().split(/\s+/); - let highlighted = text; - - terms.forEach(term => { - if (term.length > 1) { - const regex = new RegExp(`(${this.escapeRegex(term)})`, 'gi'); - highlighted = highlighted.replace(regex, `$1`); - } - }); - - return highlighted; - } - - /** - * Generate breadcrumb from document ID - */ - generateBreadcrumb(docId) { - const parts = docId.split('/').filter(part => part && part !== 'index'); - return parts.length > 0 ? parts.join(' › ') : 'Home'; - } - - /** - * Generate anchor link from heading text (Sphinx-style) - */ - generateAnchor(headingText) { - return headingText - .toLowerCase() - .replace(/[^\w\s-]/g, '') // Remove special chars - .replace(/\s+/g, '-') // Replace spaces with hyphens - .trim(); - } - - /** - * Get document URL from result object - */ - getDocumentUrl(result) { - if (result.url) { - return result.url; - } - return `${result.id.replace(/^\/+/, '')}.html`; - } - - /** - * Get appropriate icon for section type - */ - getSectionIcon(type, level) { - switch (type) { - case 'title': - return ''; - case 'heading': - if (level <= 2) return ''; - if (level <= 4) return ''; - return ''; - case 'content': - return ''; - default: - return ''; - } - } - - /** - * Load external script (like Lunr.js) - */ - async loadScript(src) { - return new Promise((resolve, reject) => { - const script = document.createElement('script'); - script.src = src; - script.onload = resolve; - script.onerror = reject; - document.head.appendChild(script); - }); - } - - /** - * Safe substring with fallback - */ - safeSubstring(str, maxLength = 200, fallback = '') { - if (!str) return fallback; - return str.length > maxLength ? str.substring(0, maxLength) : str; - } - - /** - * Check if string is valid and not empty - */ - isValidString(str) { - return typeof str === 'string' && str.trim().length > 0; - } - - /** - * Safe array access with fallback - */ - safeArray(arr, fallback = []) { - return Array.isArray(arr) ? arr : fallback; - } -} - -// Make Utils available globally -window.Utils = Utils; diff --git a/docs/_ext/search_assets/templates/search.html b/docs/_ext/search_assets/templates/search.html deleted file mode 100644 index 7f3ba378a..000000000 --- a/docs/_ext/search_assets/templates/search.html +++ /dev/null @@ -1,49 +0,0 @@ -{%- extends "page.html" %} -{# Enhanced Search Page - Clean template without embedded CSS/JS #} - -{% block docs_body %} -
-

{{ _("Search") }}

- - - - {# Search and filter controls container - will be enhanced by JavaScript #} -
- -
- - {# Search results container #} -
-
- -

Search Documentation

-

Start typing to search across all documentation pages...

-
- - - Search Tips: Use specific terms for better results • Search includes titles, content, and - headings - -
-
-
-
-{% endblock docs_body %} - -{# Page metadata #} -{%- block htmltitle -%} -{{ _("Search") }} - {{ title or docstitle }} -{%- endblock htmltitle -%} - -{# Load our enhanced search scripts #} -{% block scripts -%} -{{ super() }} -{# Search page script is loaded via html_js_files in conf.py #} -{%- endblock scripts %} diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html deleted file mode 100644 index dbca49e8c..000000000 --- a/docs/_templates/layout.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "!layout.html" %} - -{% block extrahead %} -{{ super() }} - -{% endblock %} - -{% block footer %} -{{ super() }} - -{% endblock %} diff --git a/docs/about/architecture.md b/docs/about/architecture.md deleted file mode 100644 index 07864bb33..000000000 --- a/docs/about/architecture.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -title: - page: How OpenShell Works - nav: How It Works -description: OpenShell architecture overview covering the gateway, sandbox, policy engine, and privacy router. -topics: -- Generative AI -- Cybersecurity -tags: -- AI Agents -- Sandboxing -- Security -- Architecture -content: - type: concept - difficulty: technical_beginner - audience: - - engineer - - data_scientist ---- - - - -# How OpenShell Works - -OpenShell runs inside a Docker container. Each sandbox is an isolated environment managed through the gateway. Four components work together to keep agents secure. - -```{figure} architecture.svg -:alt: OpenShell architecture diagram showing the component layout -:align: center -:target: ../_images/architecture.svg -``` - -## Components - -The following table describes each component and its role in the system: - -| Component | Role | -|---|---| -| **Gateway** | Control-plane API that coordinates sandbox lifecycle and state, acts as the auth boundary, and brokers requests across the platform. | -| **Sandbox** | Isolated runtime that includes container supervision and policy-enforced egress routing. | -| **Policy Engine** | Policy definition and enforcement layer for filesystem, network, and process constraints. Defense in depth enforces policies from the application layer down to infrastructure and kernel layers. | -| **Privacy Router** | Privacy-aware LLM routing layer that keeps sensitive context on sandbox compute and routes based on cost and privacy policy. | - -## How a Request Flows - -Every outbound connection from agent code passes through the same decision path: - -1. The agent process opens an outbound connection (API call, package install, git clone, and so on). -2. The proxy inside the sandbox intercepts the connection and identifies which binary opened it. -3. If the target is `https://inference.local`, the proxy handles it as managed inference before policy evaluation. OpenShell strips sandbox-supplied credentials, injects the configured backend credentials, and forwards the request to the managed model endpoint. -4. For every other destination, the proxy queries the policy engine with the destination, port, and calling binary. -5. The policy engine returns one of two decisions: - - **Allow** - the destination and binary match a policy block. Traffic flows directly to the external service. - - **Deny** - no policy block matched. The connection is blocked and logged. - -For REST endpoints with TLS termination enabled, the proxy also decrypts TLS and checks each HTTP request against per-method, per-path rules before allowing it through. - -## Deployment Modes - -OpenShell can run locally, on a remote host, or behind a cloud proxy. The architecture is identical in all cases — only the Docker container location and authentication mode change. - -| Mode | Description | Command | -|---|---|---| -| **Local** | The gateway runs inside Docker on your workstation. The CLI provisions it automatically on first use. | `openshell gateway start` | -| **Remote** | The gateway runs on a remote host via SSH. Only Docker is required on the remote machine. | `openshell gateway start --remote user@host` | -| **Cloud** | A gateway already running behind a reverse proxy (e.g. Cloudflare Access). Register and authenticate via browser. | `openshell gateway add https://gateway.example.com` | - -You can register multiple gateways and switch between them with `openshell gateway select`. For the full deployment and management workflow, refer to the [Gateways](../sandboxes/manage-gateways.md) section. - -## Next Steps - -Continue with one of the following: - -- To deploy or register a gateway, refer to [Gateways](../sandboxes/manage-gateways.md). -- To create your first sandbox, refer to the [Quickstart](../get-started/quickstart.md). -- To learn how OpenShell enforces isolation across all protection layers, refer to [Sandboxes](../sandboxes/index.md). diff --git a/fern/pages/about/architecture.mdx b/docs/about/architecture.mdx similarity index 94% rename from fern/pages/about/architecture.mdx rename to docs/about/architecture.mdx index cd49890bd..0c701e6a9 100644 --- a/fern/pages/about/architecture.mdx +++ b/docs/about/architecture.mdx @@ -1,17 +1,12 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "How OpenShell Works" sidebar-title: "How It Works" description: "OpenShell architecture overview covering the gateway, sandbox, policy engine, and privacy router." keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing, Security, Architecture" -tags: - - AI Agents - - Sandboxing - - Security - - Architecture position: 2 --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} OpenShell runs inside a Docker container. Each sandbox is an isolated environment managed through the gateway. Four components work together to keep agents secure. diff --git a/docs/about/architecture.svg b/docs/about/architecture.svg deleted file mode 100644 index b0bcd4d5d..000000000 --- a/docs/about/architecture.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/about/overview.md b/docs/about/overview.md deleted file mode 100644 index 0d0242d9e..000000000 --- a/docs/about/overview.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: - page: Overview of NVIDIA OpenShell - nav: Overview -description: OpenShell is the safe, private runtime for autonomous AI agents. Run agents in sandboxed environments that protect your data, credentials, and infrastructure. -topics: -- Generative AI -- Cybersecurity -tags: -- AI Agents -- Sandboxing -- Security -- Privacy -- Inference Routing -content: - type: concept - difficulty: technical_beginner - audience: - - engineer - - data_scientist ---- - - - -# Overview of NVIDIA OpenShell - -NVIDIA OpenShell is an open-source runtime for executing autonomous AI agents in sandboxed environments with kernel-level isolation. It combines sandbox runtime controls and a declarative YAML policy so teams can run agents without giving them unrestricted access to local files, credentials, and external networks. - -## Why OpenShell Exists - -AI agents are most useful when they can read files, install packages, call APIs, and use credentials. That same access can create material risk. OpenShell is designed for this tradeoff: preserve agent capability while enforcing explicit controls over what the agent can access. - -## Common Risks and Controls - -The table below summarizes common failure modes and how OpenShell mitigates them. - -| Threat | Without controls | With OpenShell | -|---|---|---| -| Data exfiltration | Agent uploads source code or internal files to unauthorized endpoints. | Network policies allow only approved destinations; other outbound traffic is denied. | -| Credential theft | Agent reads local secrets such as SSH keys or cloud credentials. | Filesystem restrictions (Landlock) confine access to declared paths only. | -| Unauthorized API usage | Agent sends prompts or data to unapproved model providers. | Privacy routing and network policies control where inference traffic can go. | -| Privilege escalation | Agent attempts `sudo`, setuid paths, or dangerous syscall behavior. | Unprivileged process identity and seccomp restrictions block escalation paths. | - -## Protection Layers at a Glance - -OpenShell applies defense in depth across the following policy domains. - -| Layer | What it protects | When it applies | -|---|---|---| -| Filesystem | Prevents reads/writes outside allowed paths. | Locked at sandbox creation. | -| Network | Blocks unauthorized outbound connections. | Hot-reloadable at runtime. | -| Process | Blocks privilege escalation and dangerous syscalls. | Locked at sandbox creation. | -| Inference | Reroutes model API calls to controlled backends. | Hot-reloadable at runtime. | - -For details, refer to [Sandbox Policies](../sandboxes/index.md#sandbox-policies) and [Customize Sandbox Policies](../sandboxes/policies.md). - -## Common Use Cases - -OpenShell supports a range of agent deployment patterns. - -| Use Case | Description | -|-----------------------------|----------------------------------------------------------------------------------------------------------| -| Secure coding agents | Run Claude Code, OpenCode, or OpenClaw with constrained file and network access. | -| Private enterprise development | Route inference to self-hosted or private backends while keeping sensitive context under your control. | -| Compliance and audit | Treat policy YAML as version-controlled security controls that can be reviewed and audited. | -| Reusable environments | Use community sandbox images or bring your own containerized runtime. | - ---- - -## Next Steps - -Explore these topics to go deeper: - -- To understand the components that make up the OpenShell runtime, refer to the [Architecture Overview](architecture.md). -- To install the CLI and create your first sandbox, refer to the [Quickstart](../get-started/quickstart.md). -- To learn how OpenShell enforces isolation across all protection layers, refer to [Sandboxes](../sandboxes/index.md). diff --git a/fern/pages/about/overview.mdx b/docs/about/overview.mdx similarity index 94% rename from fern/pages/about/overview.mdx rename to docs/about/overview.mdx index 9bdbf0c05..6255c6225 100644 --- a/fern/pages/about/overview.mdx +++ b/docs/about/overview.mdx @@ -1,18 +1,12 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Overview of NVIDIA OpenShell" sidebar-title: "Overview" description: "OpenShell is the safe, private runtime for autonomous AI agents. Run agents in sandboxed environments that protect your data, credentials, and infrastructure." keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing, Security, Privacy, Inference Routing" -tags: - - AI Agents - - Sandboxing - - Security - - Privacy - - Inference Routing position: 1 --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} NVIDIA OpenShell is an open-source runtime for executing autonomous AI agents in sandboxed environments with kernel-level isolation. It combines sandbox runtime controls and a declarative YAML policy so teams can run agents without giving them unrestricted access to local files, credentials, and external networks. diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md deleted file mode 100644 index 9e39aaa94..000000000 --- a/docs/about/release-notes.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: - page: NVIDIA OpenShell Release Notes - nav: Release Notes -description: Track the latest changes and improvements to NVIDIA OpenShell. -topics: -- Generative AI -- Cybersecurity -tags: -- Release Notes -- Changelog -- AI Agents -content: - type: reference - difficulty: technical_beginner - audience: - - engineer - - data_scientist ---- - - - -# NVIDIA OpenShell Release Notes - -NVIDIA OpenShell follows a frequent release cadence. Use the following GitHub resources directly. - -| Resource | Description | -|---|---| -| [Releases](https://github.com/NVIDIA/OpenShell/releases) | Versioned release notes and downloadable assets. | -| [Release comparison](https://github.com/NVIDIA/OpenShell/compare) | Diff between any two tags or branches. | -| [Merged pull requests](https://github.com/NVIDIA/OpenShell/pulls?q=is%3Apr+is%3Amerged) | Individual changes with review discussion. | -| [Commit history](https://github.com/NVIDIA/OpenShell/commits/main) | Full commit log on `main`. | diff --git a/fern/pages/about/release-notes.mdx b/docs/about/release-notes.mdx similarity index 80% rename from fern/pages/about/release-notes.mdx rename to docs/about/release-notes.mdx index 9830c28e0..0d9a4407e 100644 --- a/fern/pages/about/release-notes.mdx +++ b/docs/about/release-notes.mdx @@ -1,16 +1,12 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "NVIDIA OpenShell Release Notes" sidebar-title: "Release Notes" description: "Track the latest changes and improvements to NVIDIA OpenShell." keywords: "Generative AI, Cybersecurity, Release Notes, Changelog, AI Agents" -tags: - - Release Notes - - Changelog - - AI Agents position: 4 --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} NVIDIA OpenShell follows a frequent release cadence. Use the following GitHub resources directly. diff --git a/docs/about/supported-agents.md b/docs/about/supported-agents.md deleted file mode 100644 index 664156ada..000000000 --- a/docs/about/supported-agents.md +++ /dev/null @@ -1,16 +0,0 @@ -# Supported Agents - -The following table summarizes the agents that run in OpenShell sandboxes. All agent sandbox images are maintained in the [OpenShell Community](https://github.com/NVIDIA/OpenShell-Community) repository. Agents in the base image are auto-configured when passed as the trailing command to `openshell sandbox create`. - -| Agent | Source | Default Policy | Notes | -|---|---|---|---| -| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Full coverage | Works out of the box. Requires `ANTHROPIC_API_KEY`. | -| [OpenCode](https://opencode.ai/) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Partial coverage | Pre-installed. Add `opencode.ai` endpoint and OpenCode binary paths to the policy for full functionality. | -| [Codex](https://developers.openai.com/codex) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | No coverage | Pre-installed. Requires a custom policy with OpenAI endpoints and Codex binary paths. Requires `OPENAI_API_KEY`. | -| [GitHub Copilot CLI](https://docs.github.com/en/copilot/github-copilot-in-the-cli) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Full coverage | Pre-installed. Works out of the box. Requires `GITHUB_TOKEN` or `COPILOT_GITHUB_TOKEN`. | -| [OpenClaw](https://openclaw.ai/) | [`openclaw`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/openclaw) | Bundled | Agent orchestration layer. Launch with `openshell sandbox create --from openclaw`. | -| [Ollama](https://ollama.com/) | [`ollama`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/ollama) | Bundled | Run cloud and local models. Includes Claude Code, Codex, and OpenCode. Launch with `openshell sandbox create --from ollama`. | - -More community agent sandboxes are available in the {doc}`../sandboxes/community-sandboxes` catalog. - -For a complete support matrix, refer to the {doc}`../reference/support-matrix` page. diff --git a/fern/pages/about/supported-agents.mdx b/docs/about/supported-agents.mdx similarity index 94% rename from fern/pages/about/supported-agents.mdx rename to docs/about/supported-agents.mdx index 10c7063fe..e65778ab9 100644 --- a/fern/pages/about/supported-agents.mdx +++ b/docs/about/supported-agents.mdx @@ -1,10 +1,9 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Supported Agents" description: "AI agent frameworks and runtimes compatible with OpenShell sandboxes." keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing, Claude, Codex, Cursor" -tags: - - AI Agents - - Sandboxing position: 3 --- The following table summarizes the agents that run in OpenShell sandboxes. All agent sandbox images are maintained in the [OpenShell Community](https://github.com/NVIDIA/OpenShell-Community) repository. Agents in the base image are auto-configured when passed as the trailing command to `openshell sandbox create`. diff --git a/docs/assets/openshell-terminal.png b/docs/assets/openshell-terminal.png deleted file mode 100644 index 09fe2b768..000000000 Binary files a/docs/assets/openshell-terminal.png and /dev/null differ diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 9afa14409..000000000 --- a/docs/conf.py +++ /dev/null @@ -1,121 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import sys -from datetime import date -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent.parent)) -sys.path.insert(0, str(Path(__file__).parent / "_ext")) - -project = "NVIDIA OpenShell Developer Guide" -this_year = date.today().year -copyright = f"2025-{this_year}, NVIDIA Corporation" -author = "NVIDIA Corporation" -release = "latest" - -extensions = [ - "myst_parser", - "sphinx.ext.autodoc", - "sphinx.ext.autosummary", - "sphinx.ext.napoleon", - "sphinx.ext.viewcode", - "sphinx.ext.intersphinx", - "sphinx_copybutton", - "sphinx_design", - "sphinxcontrib.mermaid", - "policy_table", - "json_output", - "search_assets", -] - -autodoc_default_options = { - "members": True, - "undoc-members": False, - "show-inheritance": True, - "member-order": "bysource", -} -autodoc_typehints = "description" -autodoc_class_signature = "separated" - -copybutton_exclude = ".linenos, .gp, .go" - -exclude_patterns = [ - "README.md", - "SETUP.md", - "CONTRIBUTING.md", - "_build/**", - "_ext/**", -] - -myst_linkify_fuzzy_links = False -myst_heading_anchors = 4 -myst_enable_extensions = [ - "colon_fence", - "deflist", - "dollarmath", - "fieldlist", - "substitution", -] -myst_links_external_new_tab = True - -myst_substitutions = { - "version": release, -} - -templates_path = ["_templates"] - -html_theme = "nvidia_sphinx_theme" -html_copy_source = False -html_show_sourcelink = False -html_show_sphinx = False - -mermaid_init_js = ( - "mermaid.initialize({" - " startOnLoad: true," - " theme: 'base'," - " themeVariables: {" - " background: '#ffffff'," - " primaryColor: '#76b900'," - " primaryTextColor: '#000000'," - " primaryBorderColor: '#000000'," - " lineColor: '#000000'," - " textColor: '#000000'," - " mainBkg: '#ffffff'," - " nodeBorder: '#000000'" - " }" - "});" -) - -html_domain_indices = False -html_use_index = False -html_extra_path = ["project.json"] -highlight_language = "console" - -html_theme_options = { - "announcement": ( - "🔔 NVIDIA OpenShell is alpha software. APIs and behavior" - " may change without notice. Do not use in production." - ), - "icon_links": [ - { - "name": "GitHub", - "url": "https://github.com/NVIDIA/OpenShell", - "icon": "fa-brands fa-github", - "type": "fontawesome", - }, - { - "name": "PyPI", - "url": "https://pypi.org/project/openshell/", - "icon": "fa-brands fa-python", - "type": "fontawesome", - }, - ], -} - -html_baseurl = "https://docs.nvidia.com/openshell/latest/" - -json_output_settings = { - "enabled": True, - "verbose": True, -} diff --git a/docs/get-started/quickstart.md b/docs/get-started/quickstart.md deleted file mode 100644 index 5f3607c1b..000000000 --- a/docs/get-started/quickstart.md +++ /dev/null @@ -1,173 +0,0 @@ ---- -title: - page: Quickstart - nav: Quickstart -description: Install the OpenShell CLI and create your first sandboxed AI agent in two commands. -topics: -- Generative AI -- Cybersecurity -tags: -- AI Agents -- Sandboxing -- Installation -- Quickstart -content: - type: get_started - difficulty: technical_beginner - audience: - - engineer - - data_scientist ---- - - - -# Quickstart - -This page gets you from zero to a running, policy-enforced sandbox in two commands. - -## Prerequisites - -Before you begin, make sure you have: - -- Docker Desktop running on your machine. - -For a complete list of requirements, refer to {doc}`../reference/support-matrix`. - -## Install the OpenShell CLI - -Run the install script: - -```console -$ curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh -``` - -If you prefer [uv](https://docs.astral.sh/uv/): - -```console -$ uv tool install -U openshell -``` - -After installing the CLI, run `openshell --help` in your terminal to see the full CLI reference, including all commands and flags. - -:::{tip} -You can also clone the [NVIDIA OpenShell GitHub repository](https://github.com/NVIDIA/OpenShell) and use the `/openshell-cli` skill to load the CLI reference into your agent. -::: - -## Create Your First OpenShell Sandbox - -Create a sandbox and launch an agent inside it. -Choose the tab that matches your agent: - -::::{tab-set} - -:::{tab-item} Claude Code - -Run the following command to create a sandbox with Claude Code: - -```console -$ openshell sandbox create -- claude -``` - -The CLI prompts you to create a provider from local credentials. -Type `yes` to continue. -If `ANTHROPIC_API_KEY` is set in your environment, the CLI picks it up automatically. -If not, you can configure it from inside the sandbox after it launches. -::: - -:::{tab-item} OpenCode - -Run the following command to create a sandbox with OpenCode: - -```console -$ openshell sandbox create -- opencode -``` - -The CLI prompts you to create a provider from local credentials. -Type `yes` to continue. -If `OPENAI_API_KEY` or `OPENROUTER_API_KEY` is set in your environment, the CLI picks it up automatically. -If not, you can configure it from inside the sandbox after it launches. -::: - -:::{tab-item} Codex - -Run the following command to create a sandbox with Codex: - -```console -$ openshell sandbox create -- codex -``` - -The CLI prompts you to create a provider from local credentials. -Type `yes` to continue. -If `OPENAI_API_KEY` is set in your environment, the CLI picks it up automatically. -If not, you can configure it from inside the sandbox after it launches. -::: - -:::{tab-item} OpenClaw - -Run the following command to create a sandbox with OpenClaw: - -```console -$ openshell sandbox create --from openclaw -``` - -The `--from` flag pulls a pre-built sandbox definition from the [OpenShell Community](https://github.com/NVIDIA/OpenShell-Community) catalog. -Each definition bundles a container image, a tailored policy, and optional skills into a single package. -::: - -:::{tab-item} Community Sandbox - -Use the `--from` flag to pull other OpenShell sandbox images from the [NVIDIA Container Registry](https://registry.nvidia.com/). -For example, to pull the `base` image, run the following command: - -```console -$ openshell sandbox create --from base -``` - -::: - -:::: - -## Deploy a Gateway (Optional) - -Running `openshell sandbox create` without a gateway auto-bootstraps a local one. -To start the gateway explicitly or deploy to a remote host, choose the tab that matches your setup. - -:::::{tab-set} - -::::{tab-item} Brev - -:::{note} -Deploy an OpenShell gateway on Brev by clicking **Deploy** on the [OpenShell Launchable](https://brev.nvidia.com/launchable/deploy/now?launchableID=env-3Ap3tL55zq4a8kew1AuW0FpSLsg). -::: - -After the instance starts running, find the gateway URL in the Brev console under **Using Secure Links**. -Copy the shareable URL for **port 8080**, which is the gateway endpoint. - -```console -$ openshell gateway add https://.brevlab.com -$ openshell status -``` - -:::: - -::::{tab-item} DGX Spark - -:::{note} -Set up your Spark with NVIDIA Sync first, or make sure SSH access is configured (such as SSH keys added to the host). -::: - -Deploy to a DGX Spark machine over SSH: - -```console -$ openshell gateway start --remote @.local -$ openshell status -``` - -After `openshell status` shows the gateway as healthy, all subsequent commands route through the SSH tunnel. - -:::: - -::::: diff --git a/fern/pages/get-started/quickstart.mdx b/docs/get-started/quickstart.mdx similarity index 95% rename from fern/pages/get-started/quickstart.mdx rename to docs/get-started/quickstart.mdx index f21390dec..2f26c7bfb 100644 --- a/fern/pages/get-started/quickstart.mdx +++ b/docs/get-started/quickstart.mdx @@ -1,16 +1,11 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Quickstart" description: "Install the OpenShell CLI and create your first sandboxed AI agent in two commands." keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing, Installation, Quickstart" -tags: - - AI Agents - - Sandboxing - - Installation - - Quickstart position: 1 --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} This page gets you from zero to a running, policy-enforced sandbox in two commands. diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 0945e5234..000000000 --- a/docs/index.md +++ /dev/null @@ -1,283 +0,0 @@ ---- -title: - page: NVIDIA OpenShell Developer Guide - nav: Get Started - card: NVIDIA OpenShell -description: OpenShell is the safe, private runtime for autonomous AI agents. Run agents in sandboxed environments that protect your data, credentials, and infrastructure. -topics: -- Generative AI -- Cybersecurity -tags: -- AI Agents -- Sandboxing -- Security -- Privacy -- Inference Routing -content: - type: index ---- - - - -# NVIDIA OpenShell - -[![GitHub](https://img.shields.io/badge/github-repo-green?logo=github)](https://github.com/NVIDIA/OpenShell) -[![License](https://img.shields.io/badge/License-Apache_2.0-blue)](https://github.com/NVIDIA/OpenShell/blob/main/LICENSE) -[![PyPI](https://img.shields.io/badge/PyPI-openshell-orange?logo=pypi)](https://pypi.org/project/openshell/) - -NVIDIA OpenShell is the safe, private runtime for autonomous AI agents. It provides sandboxed execution environments -that protect your data, credentials, and infrastructure. Agents run with exactly the permissions they need and -nothing more, governed by declarative policies that prevent unauthorized file access, data exfiltration, and -uncontrolled network activity. - -## Get Started - -Install the CLI and create your first sandbox in two commands. - -```{raw} html - -
-
- - - -
-
-
$ uv tool install -U openshell
-
$ openshell sandbox create -- claude--from openclaw-- opencode-- codex
-
-
-``` - -Refer to the [Quickstart](get-started/quickstart.md) for more details. - ---- - -## Explore - -::::{grid} 2 2 3 3 -:gutter: 3 - -:::{grid-item-card} About OpenShell -:link: about/overview -:link-type: doc - -Learn about OpenShell and its capabilities. - -+++ -{bdg-secondary}`Concept` -::: - -:::{grid-item-card} Quickstart -:link: get-started/quickstart -:link-type: doc - -Install the CLI and create your first sandbox in two commands. - -+++ -{bdg-secondary}`Tutorial` -::: - -:::{grid-item-card} Tutorials -:link: tutorials/index -:link-type: doc - -Hands-on walkthroughs from first sandbox to custom policies. - -+++ -{bdg-secondary}`Tutorial` -::: - -:::{grid-item-card} Gateways and Sandboxes -:link: sandboxes/manage-gateways -:link-type: doc - -Deploy gateways, create sandboxes, configure policies, providers, and community images for your AI agents. - -+++ -{bdg-secondary}`Concept` -::: - -:::{grid-item-card} Inference Routing -:link: inference/index -:link-type: doc - -Keep inference traffic private by routing API calls to local or self-hosted backends. - -+++ -{bdg-secondary}`Concept` -::: - -:::{grid-item-card} Observability -:link: observability/index -:link-type: doc - -Understand sandbox logs, access them via CLI and TUI, and export OCSF JSON records. - -+++ -{bdg-secondary}`How-To` -::: - -:::{grid-item-card} Reference -:link: reference/default-policy -:link-type: doc - -Policy schema, environment variables, and system architecture. - -+++ -{bdg-secondary}`Reference` -::: - -:::{grid-item-card} Security Best Practices -:link: security/best-practices -:link-type: doc - -Every configurable security control, its default, and the risk of changing it. - -+++ -{bdg-secondary}`Concept` -::: - -:::: - ---- - -```{admonition} Notice and Disclaimer -:class: warning - -This software automatically retrieves, accesses or interacts with external materials. Those retrieved materials are not distributed with this software and are governed solely by separate terms, conditions and licenses. You are solely responsible for finding, reviewing and complying with all applicable terms, conditions, and licenses, and for verifying the security, integrity and suitability of any retrieved materials for your specific use case. This software is provided "AS IS", without warranty of any kind. The author makes no representations or warranties regarding any retrieved materials, and assumes no liability for any losses, damages, liabilities or legal consequences from your use or inability to use this software or any retrieved materials. Use this software and the retrieved materials at your own risk. -``` - -```{toctree} -:hidden: - -Home -``` - -```{toctree} -:caption: About NVIDIA OpenShell -:hidden: - -Overview -How It Works -Supported Agents -Release Notes -``` - -```{toctree} -:caption: Get Started -:hidden: - -Quickstart -tutorials/index -``` - -```{toctree} -:caption: Gateways and Sandboxes -:hidden: - -sandboxes/index -Sandboxes -Gateways -Providers -Policies -Community Sandboxes -``` - -```{toctree} -:caption: Inference Routing -:hidden: - -inference/index -inference/configure -``` - -```{toctree} -:caption: Reference -:hidden: - -reference/gateway-auth -reference/default-policy -reference/policy-schema -reference/support-matrix -``` - -```{toctree} -:caption: Observability -:hidden: - -observability/index -observability/logging -observability/accessing-logs -observability/ocsf-json-export -``` - -```{toctree} -:caption: Security -:hidden: - -security/best-practices -``` - -```{toctree} -:caption: Resources -:hidden: - -resources/license -``` diff --git a/fern/pages/index.mdx b/docs/index.mdx similarity index 92% rename from fern/pages/index.mdx rename to docs/index.mdx index 1570bcae3..8c09fffb3 100644 --- a/fern/pages/index.mdx +++ b/docs/index.mdx @@ -1,22 +1,15 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "NVIDIA OpenShell Developer Guide" -sidebar-title: "Home" slug: "get-started" description: "OpenShell is the safe, private runtime for autonomous AI agents. Run agents in sandboxed environments that protect your data, credentials, and infrastructure." keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing, Security, Privacy, Inference Routing" -tags: - - AI Agents - - Sandboxing - - Security - - Privacy - - Inference Routing position: 1 --- -import { BadgeLinks } from "@/components/BadgeLinks"; +import { BadgeLinks } from "./_components/BadgeLinks"; -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} Tutorial - + Hands-on walkthroughs from first sandbox to custom policies. @@ -129,7 +122,7 @@ Keep inference traffic private by routing API calls to local or self-hosted back Concept - + Understand sandbox logs, access them via CLI and TUI, and export OCSF JSON records. diff --git a/fern/versions/latest.yml b/docs/index.yml similarity index 57% rename from fern/versions/latest.yml rename to docs/index.yml index b1058b771..fd29fc0d8 100644 --- a/fern/versions/latest.yml +++ b/docs/index.yml @@ -1,25 +1,26 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 + navigation: - section: "Get Started" slug: get-started contents: - page: "Home" - path: ../pages/index.mdx + path: index.mdx - page: "Quickstart" - path: ../pages/get-started/quickstart.mdx - - folder: ../pages/tutorials + path: get-started/quickstart.mdx + - folder: tutorials skip-slug: true -- folder: ../pages/about +- folder: about title: "About NVIDIA OpenShell" -- folder: ../pages/sandboxes -- folder: ../pages/inference +- folder: sandboxes +- folder: inference title: "Inference Routing" -- folder: ../pages/observability +- folder: observability title: "Observability" -- folder: ../pages/reference +- folder: reference title: "Reference" -- folder: ../pages/security +- folder: security title: "Security" -- folder: ../pages/resources +- folder: resources title: "Resources" diff --git a/fern/pages/inference/about.mdx b/docs/inference/about.mdx similarity index 93% rename from fern/pages/inference/about.mdx rename to docs/inference/about.mdx index 5d2ceb390..3f638c699 100644 --- a/fern/pages/inference/about.mdx +++ b/docs/inference/about.mdx @@ -1,17 +1,12 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "About Inference Routing" sidebar-title: "About Inference Routing" description: "Understand how OpenShell routes inference traffic through external endpoints and the local privacy router." keywords: "Generative AI, Cybersecurity, Inference Routing, Privacy, AI Agents, LLM" -tags: - - Inference Routing - - Privacy - - AI Agents - - LLM position: 1 --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} NVIDIA OpenShell handles inference traffic through two endpoints: `inference.local` and external endpoints. The following table summarizes how OpenShell handles inference traffic. diff --git a/docs/inference/configure.md b/docs/inference/configure.md deleted file mode 100644 index 78065689e..000000000 --- a/docs/inference/configure.md +++ /dev/null @@ -1,214 +0,0 @@ ---- -title: - page: Configure Inference Routing - nav: Configure -description: Set up the managed local inference endpoint with provider credentials and model configuration. -topics: -- Generative AI -- Cybersecurity -tags: -- Inference Routing -- Configuration -- Privacy -- LLM -- Provider -content: - type: how_to - difficulty: technical_intermediate - audience: - - engineer ---- - - - -# Configure Inference Routing - -This page covers the managed local inference endpoint (`https://inference.local`). External inference endpoints go through sandbox `network_policies`. Refer to [Policies](/sandboxes/policies.md) for details. - -The configuration consists of three values: - -| Value | Description | -|---|---| -| Provider record | The credential backend OpenShell uses to authenticate with the upstream model host. | -| Model ID | The model to use for generation requests. | -| Timeout | Per-request timeout in seconds for upstream inference calls. Defaults to 60 seconds. | - -For a list of tested providers and their base URLs, refer to [Supported Inference Providers](../sandboxes/manage-providers.md#supported-inference-providers). - -## Create a Provider - -Create a provider that holds the backend credentials you want OpenShell to use. - -:::::{tab-set} - -::::{tab-item} NVIDIA API Catalog - -```console -$ openshell provider create --name nvidia-prod --type nvidia --from-existing -``` - -This reads `NVIDIA_API_KEY` from your environment. - -:::: - -::::{tab-item} OpenAI-compatible Provider - -Any cloud provider that exposes an OpenAI-compatible API works with the `openai` provider type. You need three values from the provider: the base URL, an API key, and a model name. - -```console -$ openshell provider create \ - --name my-cloud-provider \ - --type openai \ - --credential OPENAI_API_KEY= \ - --config OPENAI_BASE_URL=https://api.example.com/v1 -``` - -Replace the base URL and API key with the values from your provider. For supported providers out of the box, refer to [Supported Inference Providers](../sandboxes/manage-providers.md#supported-inference-providers). For other providers, refer to your provider's documentation for the correct base URL, available models, and API key setup. - -:::: - -::::{tab-item} Local Endpoint - -```console -$ openshell provider create \ - --name my-local-model \ - --type openai \ - --credential OPENAI_API_KEY=empty-if-not-required \ - --config OPENAI_BASE_URL=http://host.openshell.internal:11434/v1 -``` - -Use `--config OPENAI_BASE_URL` to point to any OpenAI-compatible server running where the gateway runs. For host-backed local inference, use `host.openshell.internal` or the host's LAN IP. Avoid `127.0.0.1` and `localhost`. Set `OPENAI_API_KEY` to a dummy value if the server does not require authentication. - -:::{tip} -For a self-contained setup, the Ollama community sandbox bundles Ollama inside the sandbox itself — no host-level provider needed. See {doc}`/tutorials/inference-ollama` for details. -::: - -Ollama also supports cloud-hosted models using the `:cloud` tag suffix (e.g., `qwen3.5:cloud`). - -:::: - -::::{tab-item} Anthropic - -```console -$ openshell provider create --name anthropic-prod --type anthropic --from-existing -``` - -This reads `ANTHROPIC_API_KEY` from your environment. - -:::: - -::::: - -## Set Inference Routing - -Point `inference.local` at that provider and choose the model to use: - -```console -$ openshell inference set \ - --provider nvidia-prod \ - --model nvidia/nemotron-3-nano-30b-a3b -``` - -To override the default 60-second per-request timeout, add `--timeout`: - -```console -$ openshell inference set \ - --provider nvidia-prod \ - --model nvidia/nemotron-3-nano-30b-a3b \ - --timeout 300 -``` - -The value is in seconds. When `--timeout` is omitted (or set to `0`), the default of 60 seconds applies. - -## Verify the Active Config - -Confirm that the provider and model are set correctly: - -```console -$ openshell inference get -Gateway inference: - - Provider: nvidia-prod - Model: nvidia/nemotron-3-nano-30b-a3b - Timeout: 300s - Version: 1 -``` - -## Update Part of the Config - -Use `update` when you want to change only one field: - -```console -$ openshell inference update --model nvidia/nemotron-3-nano-30b-a3b -``` - -Or switch providers without repeating the current model: - -```console -$ openshell inference update --provider openai-prod -``` - -Or change only the timeout: - -```console -$ openshell inference update --timeout 120 -``` - -## Use the Local Endpoint from a Sandbox - -After inference is configured, code inside any sandbox can call `https://inference.local` directly: - -```python -from openai import OpenAI - -client = OpenAI(base_url="https://inference.local/v1", api_key="unused") - -response = client.chat.completions.create( - model="anything", - messages=[{"role": "user", "content": "Hello"}], -) -``` - -The client-supplied `model` and `api_key` values are not sent upstream. The privacy router injects the real credentials from the configured provider and rewrites the model before forwarding. - -Some SDKs require a non-empty API key even though `inference.local` does not use the sandbox-provided value. In those cases, pass any placeholder such as `test` or `unused`. - -Use this endpoint when inference should stay local to the host for privacy and security reasons. External providers that should be reached directly belong in `network_policies` instead. - -When the upstream runs on the same machine as the gateway, bind it to `0.0.0.0` and point the provider at `host.openshell.internal` or the host's LAN IP. `127.0.0.1` and `localhost` usually fail because the request originates from the gateway or sandbox runtime, not from your shell. - -If the gateway runs on a remote host or behind a cloud deployment, `host.openshell.internal` points to that remote machine, not to your laptop. A locally running Ollama or vLLM process is not reachable from a remote gateway unless you add your own tunnel or shared network path. Ollama also supports cloud-hosted models that do not require local hardware. - -### Verify the Endpoint from a Sandbox - -`openshell inference set` and `openshell inference update` verify the resolved upstream endpoint by default before saving the configuration. If the endpoint is not live yet, retry with `--no-verify` to persist the route without the probe. - -`openshell inference get` confirms the current saved configuration. To confirm end-to-end connectivity from a sandbox, run: - -```bash -curl https://inference.local/v1/responses \ - -H "Content-Type: application/json" \ - -d '{ - "instructions": "You are a helpful assistant.", - "input": "Hello!" - }' -``` - -A successful response confirms the privacy router can reach the configured backend and the model is serving requests. - -- Gateway-scoped: Every sandbox using the active gateway sees the same `inference.local` backend. -- HTTPS only: `inference.local` is intercepted only for HTTPS traffic. -- Hot reload: Provider, model, and timeout changes are picked up by running sandboxes within about 5 seconds by default. No sandbox recreation is required. - -## Next Steps - -Explore related topics: - -- To understand the inference routing flow and supported API patterns, refer to {doc}`index`. -- To follow a complete Ollama-based local setup, refer to {doc}`/tutorials/inference-ollama`. -- To follow a complete LM Studio-based local setup, refer to {doc}`/tutorials/local-inference-lmstudio`. -- To control external endpoints, refer to [Policies](/sandboxes/policies.md). -- To manage provider records, refer to {doc}`../sandboxes/manage-providers`. diff --git a/fern/pages/inference/configure.mdx b/docs/inference/configure.mdx similarity index 97% rename from fern/pages/inference/configure.mdx rename to docs/inference/configure.mdx index abdb5167e..a90b377db 100644 --- a/fern/pages/inference/configure.mdx +++ b/docs/inference/configure.mdx @@ -1,18 +1,12 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Configure Inference Routing" sidebar-title: "Configure Inference Routing" description: "Set up the managed local inference endpoint with provider credentials and model configuration." keywords: "Generative AI, Cybersecurity, Inference Routing, Configuration, Privacy, LLM, Provider" -tags: - - Inference Routing - - Configuration - - Privacy - - LLM - - Provider position: 2 --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} This page covers the managed local inference endpoint (`https://inference.local`). External inference endpoints go through sandbox `network_policies`. Refer to [Policies](/sandboxes/policies) for details. diff --git a/docs/inference/index.md b/docs/inference/index.md deleted file mode 100644 index 92e2b1039..000000000 --- a/docs/inference/index.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: - page: About Inference Routing - nav: Inference Routing -description: Understand how OpenShell routes inference traffic through external endpoints and the local privacy router. -topics: -- Generative AI -- Cybersecurity -tags: -- Inference Routing -- Privacy -- AI Agents -- LLM -content: - type: concept - difficulty: technical_beginner - audience: - - engineer - - data_scientist ---- - - - -# About Inference Routing - -NVIDIA OpenShell handles inference traffic through two endpoints: `inference.local` and external endpoints. -The following table summarizes how OpenShell handles inference traffic. - -| Path | How It Works | -|---|---| -| External endpoints | Traffic to hosts like `api.openai.com` or `api.anthropic.com` is treated like any other outbound request, allowed or denied by `network_policies`. Refer to {doc}`../sandboxes/policies`. | -| `inference.local` | A special endpoint exposed inside every sandbox for routing inference traffic locally, preserving privacy and security. The {doc}`privacy router ` strips the original credentials, injects the configured backend credentials, and forwards to the managed model endpoint. | - -## How `inference.local` Works - -When code inside a sandbox calls `https://inference.local`, the privacy router routes the request to the configured backend for that gateway. The configured model is applied to generation requests, and provider credentials are supplied by OpenShell rather than by code inside the sandbox. - -If code calls an external inference host directly, that traffic is evaluated only by `network_policies`. - -| Property | Detail | -|---|---| -| Credentials | No sandbox API keys needed. Credentials come from the configured provider record. | -| Configuration | One provider and one model define sandbox inference for the active gateway. Every sandbox on that gateway sees the same `inference.local` backend. | -| Provider support | NVIDIA, any OpenAI-compatible provider, and Anthropic all work through the same endpoint. | -| Hot-refresh | OpenShell picks up provider credential changes and inference updates without recreating sandboxes. Changes propagate within about 5 seconds by default. | - -## Supported API Patterns - -Supported request patterns depend on the provider configured for `inference.local`. - -:::::{tab-set} - -::::{tab-item} OpenAI-compatible - -| Pattern | Method | Path | -|---|---|---| -| Chat Completions | `POST` | `/v1/chat/completions` | -| Completions | `POST` | `/v1/completions` | -| Responses | `POST` | `/v1/responses` | -| Model Discovery | `GET` | `/v1/models` | -| Model Discovery | `GET` | `/v1/models/*` | - -:::: - -::::{tab-item} Anthropic-compatible - -| Pattern | Method | Path | -|---|---|---| -| Messages | `POST` | `/v1/messages` | - -:::: - -::::: - -Requests to `inference.local` that do not match the configured provider's supported patterns are denied. - -## Next Steps - -Continue with one of the following: - -- To set up the backend behind `inference.local`, refer to {doc}`configure`. -- To control external endpoints, refer to [Policies](/sandboxes/policies.md). diff --git a/docs/observability/accessing-logs.md b/docs/observability/accessing-logs.md deleted file mode 100644 index 1140a9ad0..000000000 --- a/docs/observability/accessing-logs.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -title: - page: Accessing Logs - nav: Accessing Logs -description: How to view sandbox logs through the CLI, TUI, and directly on the sandbox filesystem. -topics: -- Generative AI -- Cybersecurity -tags: -- Logging -- CLI -- TUI -- Observability -content: - type: how_to - difficulty: technical_beginner - audience: - - engineer - - data_scientist ---- - - - -# Accessing Logs - -OpenShell provides three ways to access sandbox logs: the CLI, the TUI, and direct filesystem access inside the sandbox. - -## CLI - -Use `openshell logs` to stream logs from a running sandbox: - -```console -$ openshell logs smoke-l4 --source sandbox -``` - -The CLI receives logs from the gateway over gRPC. Each line includes a timestamp, source, level, and message: - -``` -[1775014132.118] [sandbox] [OCSF ] [ocsf] NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:github_api engine:opa] -[1775014132.190] [sandbox] [OCSF ] [ocsf] HTTP:GET [INFO] ALLOWED GET http://api.github.com/zen [policy:github_api] -[1775014132.690] [sandbox] [OCSF ] [ocsf] NET:OPEN [MED] DENIED /usr/bin/curl(64) -> httpbin.org:443 [policy:- engine:opa] -[1775014113.058] [sandbox] [INFO ] [openshell_sandbox] Starting sandbox -``` - -OCSF structured events show `OCSF` as the level. Standard tracing events show `INFO`, `WARN`, or `ERROR`. - -## TUI - -The TUI dashboard displays sandbox logs in real time. Logs appear in the log panel with the same format as the CLI. - -## Gateway Log Storage - -The sandbox pushes logs to the gateway over gRPC in real time. The gateway stores a bounded buffer of recent log lines per sandbox. This buffer is not persisted to disk and is lost when the gateway restarts. - -For durable log storage, use the log files inside the sandbox or enable [OCSF JSON export](ocsf-json-export.md) and ship the JSONL files to an external log aggregator. - -## Direct Filesystem Access - -Use `openshell sandbox connect` to open a shell inside the sandbox and read the log files directly: - -```console -$ openshell sandbox connect my-sandbox -sandbox@my-sandbox:~$ cat /var/log/openshell.2026-04-01.log -``` - -You can also run a one-off command without an interactive shell: - -```console -$ openshell sandbox connect my-sandbox -- cat /var/log/openshell.2026-04-01.log -``` - -The log files inside the sandbox contain the complete record, including events that may have been dropped from the gRPC push channel under load (the push channel is bounded and drops events rather than blocking). - -## Filtering by Event Type - -The shorthand format is designed for `grep`. Some useful patterns: - -```console -# All denied connections -$ grep "DENIED\|BLOCKED" /var/log/openshell.*.log - -# All network events -$ grep "OCSF NET:" /var/log/openshell.*.log - -# All L7 enforcement decisions -$ grep "OCSF HTTP:" /var/log/openshell.*.log - -# Security findings only -$ grep "OCSF FINDING:" /var/log/openshell.*.log - -# Policy changes -$ grep "OCSF CONFIG:" /var/log/openshell.*.log - -# All OCSF events (exclude standard tracing) -$ grep "^.* OCSF " /var/log/openshell.*.log - -# Events at medium severity or above -$ grep "\[MED\]\|\[HIGH\]\|\[CRIT\]\|\[FATAL\]" /var/log/openshell.*.log -``` - -## Next Steps - -- Learn how the [log formats](logging.md) work and how to read the shorthand. -- [Enable OCSF JSON export](ocsf-json-export.md) for machine-readable structured output. diff --git a/fern/pages/observability/accessing-logs.mdx b/docs/observability/accessing-logs.mdx similarity index 95% rename from fern/pages/observability/accessing-logs.mdx rename to docs/observability/accessing-logs.mdx index baadc8c92..e3599a68f 100644 --- a/fern/pages/observability/accessing-logs.mdx +++ b/docs/observability/accessing-logs.mdx @@ -1,12 +1,9 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Accessing Logs" description: "How to view sandbox logs through the CLI, TUI, and directly on the sandbox filesystem." keywords: "Generative AI, Cybersecurity, Logging, CLI, TUI, Observability" -tags: - - Logging - - CLI - - TUI - - Observability --- OpenShell provides three ways to access sandbox logs: the CLI, the TUI, and direct filesystem access inside the sandbox. diff --git a/docs/observability/index.md b/docs/observability/index.md deleted file mode 100644 index a9607457b..000000000 --- a/docs/observability/index.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: - page: Observability - nav: Observability -description: Understand how OpenShell logs sandbox activity, how to access logs, and how to export structured OCSF records. -topics: -- Generative AI -- Cybersecurity -tags: -- Logging -- OCSF -- Observability -- Monitoring -content: - type: concept - difficulty: technical_beginner - audience: - - engineer - - data_scientist ---- - - - -# Observability - -OpenShell provides structured logging for every sandbox. Every network connection, process lifecycle event, filesystem policy decision, and configuration change is recorded so you can understand exactly what happened inside a sandbox. - -This section covers: - -- **[Sandbox Logging](logging.md)** -- How the two log formats work (standard tracing and OCSF structured events), where logs are stored, and how to read them. -- **[Accessing Logs](accessing-logs.md)** -- How to view logs through the CLI, TUI, and directly on the sandbox filesystem. -- **[OCSF JSON Export](ocsf-json-export.md)** -- How to enable full OCSF JSON output for integration with SIEMs, log aggregators, and compliance tools. - -```{toctree} -:hidden: - -logging -accessing-logs -ocsf-json-export -``` diff --git a/docs/observability/logging.md b/docs/observability/logging.md deleted file mode 100644 index 680f7e16b..000000000 --- a/docs/observability/logging.md +++ /dev/null @@ -1,173 +0,0 @@ ---- -title: - page: Sandbox Logging - nav: Logging -description: How OpenShell logs sandbox activity using standard tracing and OCSF structured events. -topics: -- Generative AI -- Cybersecurity -tags: -- Logging -- OCSF -- Observability -content: - type: concept - difficulty: technical_beginner - audience: - - engineer - - data_scientist ---- - - - -# Sandbox Logging - -Every OpenShell sandbox produces a log that records network connections, process lifecycle events, filesystem policy decisions, and configuration changes. The log uses two formats depending on the type of event. - -## Log Formats - -### Standard tracing - -Internal operational events use Rust's `tracing` framework with a conventional format: - -``` -2026-04-01T03:28:39.160Z INFO openshell_sandbox: Fetching sandbox policy via gRPC -2026-04-01T03:28:39.175Z INFO openshell_sandbox: Creating OPA engine from proto policy data -``` - -These events cover startup plumbing, gRPC communication, and internal state transitions that are useful for debugging but don't represent security-relevant decisions. - -### OCSF structured events - -Network, process, filesystem, and configuration events use the [Open Cybersecurity Schema Framework (OCSF)](https://ocsf.io) format. OCSF is an open standard for normalizing security telemetry across tools and platforms. OpenShell maps sandbox events to OCSF v1.7.0 event classes. - -In the log file, OCSF events appear in a shorthand format with an `OCSF` level label, designed for quick human and agent scanning: - -``` -2026-04-01T04:04:13.058Z INFO openshell_sandbox: Starting sandbox -2026-04-01T04:04:13.065Z OCSF CONFIG:DISCOVERY [INFO] Server returned no policy; attempting local discovery -2026-04-01T04:04:13.074Z INFO openshell_sandbox: Creating OPA engine from proto policy data -2026-04-01T04:04:13.078Z OCSF CONFIG:VALIDATED [INFO] Validated 'sandbox' user exists in image -2026-04-01T04:04:32.118Z OCSF NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:github_api engine:opa] -2026-04-01T04:04:32.190Z OCSF HTTP:GET [INFO] ALLOWED GET http://api.github.com/zen [policy:github_api] -2026-04-01T04:04:32.690Z OCSF NET:OPEN [MED] DENIED /usr/bin/curl(64) -> httpbin.org:443 [policy:- engine:opa] -``` - -The `OCSF` label at column 25 distinguishes structured events from standard `INFO` tracing at the same position. Both formats appear in the same file. - -When viewed through the CLI or TUI (which receive logs via gRPC), the same distinction applies: - -``` -[1775014132.118] [sandbox] [OCSF ] [ocsf] NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:github_api engine:opa] -[1775014132.690] [sandbox] [OCSF ] [ocsf] NET:OPEN [MED] DENIED /usr/bin/curl(64) -> httpbin.org:443 [policy:- engine:opa] -[1775014113.058] [sandbox] [INFO ] [openshell_sandbox] Starting sandbox -``` - -## OCSF Event Classes - -OpenShell maps sandbox events to these OCSF classes: - -| Shorthand prefix | OCSF class | Class UID | What it covers | -|---|---|---|---| -| `NET:` | Network Activity | 4001 | TCP proxy CONNECT tunnels, bypass detection, DNS failures | -| `HTTP:` | HTTP Activity | 4002 | HTTP FORWARD requests, L7 enforcement decisions | -| `SSH:` | SSH Activity | 4007 | SSH handshakes, authentication, channel operations | -| `PROC:` | Process Activity | 1007 | Process start, exit, timeout, signal failures | -| `FINDING:` | Detection Finding | 2004 | Security findings (nonce replay, proxy bypass, unsafe policy) | -| `CONFIG:` | Device Config State Change | 5019 | Policy load/reload, Landlock, TLS setup, inference routes | -| `LIFECYCLE:` | Application Lifecycle | 6002 | Sandbox supervisor start, SSH server ready | - -## Reading the Shorthand Format - -The shorthand format follows this pattern: - -``` -CLASS:ACTIVITY [SEVERITY] ACTION DETAILS [CONTEXT] -``` - -### Components - -**Class and activity** (`NET:OPEN`, `HTTP:GET`, `PROC:LAUNCH`) identify the OCSF event class and what happened. The class name always starts at the same column position for vertical scanning. - -**Severity** indicates the OCSF severity of the event: - -| Tag | Meaning | When used | -|---|---|---| -| `[INFO]` | Informational | Allowed connections, successful operations | -| `[LOW]` | Low | DNS failures, operational warnings | -| `[MED]` | Medium | Denied connections, policy violations | -| `[HIGH]` | High | Security findings (nonce replay, bypass detection) | -| `[CRIT]` | Critical | Process timeout kills | -| `[FATAL]` | Fatal | Unrecoverable failures | - -**Action** (`ALLOWED`, `DENIED`, `BLOCKED`) is the security control disposition. Not all events have an action (informational config events, for example). - -**Details** vary by event class: - -- Network: `process(pid) -> host:port` with the process identity and destination -- HTTP: `METHOD url` with the HTTP method and target -- SSH: peer address and authentication type -- Process: `name(pid)` with exit code or command line -- Config: description of what changed -- Finding: quoted title with confidence level - -**Context** (in brackets at the end) provides the policy rule and enforcement engine that produced the decision. - -### Examples - -An allowed HTTPS connection: -``` -OCSF NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:github_api engine:opa] -``` - -An L7 read-only policy denying a POST: -``` -OCSF HTTP:POST [MED] DENIED POST http://api.github.com/user/repos [policy:github_api] -``` - -A connection denied because no policy matched: -``` -OCSF NET:OPEN [MED] DENIED /usr/bin/curl(64) -> httpbin.org:443 [policy:- engine:opa] -``` - -Proxy and SSH servers ready: -``` -OCSF NET:LISTEN [INFO] 10.200.0.1:3128 -OCSF SSH:LISTEN [INFO] 0.0.0.0:2222 -``` - -An SSH handshake accepted (one event per connection): -``` -OCSF SSH:OPEN [INFO] ALLOWED 10.42.0.52:42706 [auth:NSSH1] -``` - -A process launched inside the sandbox: -``` -OCSF PROC:LAUNCH [INFO] sleep(49) -``` - -A policy reload after a settings change: -``` -OCSF CONFIG:DETECTED [INFO] Settings poll: config change detected [old_revision:2915564174587774909 new_revision:11008534403127604466 policy_changed:true] -OCSF CONFIG:LOADED [INFO] Policy reloaded successfully [policy_hash:0cc0c2b525573c07] -``` - -## Log File Location - -Inside the sandbox, logs are written to `/var/log/`: - -| File | Format | Rotation | -|---|---|---| -| `openshell.YYYY-MM-DD.log` | Shorthand + standard tracing | Daily, 3 files max | -| `openshell-ocsf.YYYY-MM-DD.log` | OCSF JSONL (when enabled) | Daily, 3 files max | - -Both files rotate daily and retain the 3 most recent files to bound disk usage. - -## Next Steps - -- [Access logs](accessing-logs.md) through the CLI, TUI, or sandbox filesystem. -- [Enable OCSF JSON export](ocsf-json-export.md) for SIEM integration and compliance. -- Learn about [network policies](../sandboxes/policies.md) that generate these events. diff --git a/fern/pages/observability/logging.mdx b/docs/observability/logging.mdx similarity index 97% rename from fern/pages/observability/logging.mdx rename to docs/observability/logging.mdx index abdcdfb56..4bc194d22 100644 --- a/fern/pages/observability/logging.mdx +++ b/docs/observability/logging.mdx @@ -1,12 +1,10 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Sandbox Logging" sidebar-title: "Logging" description: "How OpenShell logs sandbox activity using standard tracing and OCSF structured events." keywords: "Generative AI, Cybersecurity, Logging, OCSF, Observability" -tags: - - Logging - - OCSF - - Observability --- Every OpenShell sandbox produces a log that records network connections, process lifecycle events, filesystem policy decisions, and configuration changes. The log uses two formats depending on the type of event. diff --git a/docs/observability/ocsf-json-export.md b/docs/observability/ocsf-json-export.md deleted file mode 100644 index 696ae264f..000000000 --- a/docs/observability/ocsf-json-export.md +++ /dev/null @@ -1,187 +0,0 @@ ---- -title: - page: OCSF JSON Export - nav: OCSF JSON Export -description: How to enable full OCSF JSON logging for SIEM integration, compliance, and structured analysis. -topics: -- Generative AI -- Cybersecurity -tags: -- OCSF -- JSON -- SIEM -- Compliance -- Observability -content: - type: how_to - difficulty: technical_intermediate - audience: - - engineer ---- - - - -# OCSF JSON Export - -The [shorthand log format](logging.md) is optimized for humans and agents reading logs in real time. For machine consumption, compliance archival, or SIEM integration, you can enable full OCSF JSON export. This writes every OCSF event as a complete JSON record in JSONL format (one JSON object per line). - -## Enable JSON Export - -Use the `ocsf_json_enabled` setting to toggle JSON export. The setting can be applied globally (all sandboxes) or per-sandbox. - -Global: - -```console -$ openshell settings set --global --key ocsf_json_enabled --value true -``` - -Per-sandbox: - -```console -$ openshell settings set my-sandbox --key ocsf_json_enabled --value true -``` - -The setting takes effect on the next poll cycle (default: 10 seconds). No sandbox restart is required. - -To disable: - -```console -$ openshell settings set --global --key ocsf_json_enabled --value false -``` - -## Output Location - -When enabled, OCSF JSON records are written to `/var/log/openshell-ocsf.YYYY-MM-DD.log` inside the sandbox. The file rotates daily and retains the 3 most recent files, matching the main log file rotation. - -## JSON Record Structure - -Each line is a complete OCSF v1.7.0 JSON object. Here is an example of a network connection event: - -```json -{ - "class_uid": 4001, - "class_name": "Network Activity", - "category_uid": 4, - "category_name": "Network Activity", - "activity_id": 1, - "activity_name": "Open", - "severity_id": 1, - "severity": "Informational", - "status_id": 1, - "status": "Success", - "time": 1775014138811, - "message": "CONNECT allowed api.github.com:443", - "metadata": { - "product": { - "name": "OpenShell Sandbox Supervisor", - "vendor_name": "NVIDIA", - "version": "0.3.0" - }, - "version": "1.7.0" - }, - "action_id": 1, - "action": "Allowed", - "disposition_id": 1, - "disposition": "Allowed", - "dst_endpoint": { - "domain": "api.github.com", - "port": 443 - }, - "src_endpoint": { - "ip": "10.42.0.31", - "port": 37494 - }, - "actor": { - "process": { - "name": "/usr/bin/curl", - "pid": 57 - } - }, - "firewall_rule": { - "name": "github_api", - "type": "opa" - } -} -``` - -And a denied connection: - -```json -{ - "class_uid": 4001, - "class_name": "Network Activity", - "activity_id": 1, - "activity_name": "Open", - "severity_id": 3, - "severity": "Medium", - "status_id": 2, - "status": "Failure", - "action_id": 2, - "action": "Denied", - "disposition_id": 2, - "disposition": "Blocked", - "message": "CONNECT denied httpbin.org:443", - "dst_endpoint": { - "domain": "httpbin.org", - "port": 443 - }, - "actor": { - "process": { - "name": "/usr/bin/curl", - "pid": 63 - } - }, - "firewall_rule": { - "name": "-", - "type": "opa" - } -} -``` - -:::{note} -The JSON examples above are formatted for readability. The actual JSONL file contains one JSON object per line with no whitespace formatting. -::: - -## OCSF Event Classes in JSON - -The `class_uid` field identifies the event type: - -| `class_uid` | Class | Shorthand prefix | -|---|---|---| -| 4001 | Network Activity | `NET:` | -| 4002 | HTTP Activity | `HTTP:` | -| 4007 | SSH Activity | `SSH:` | -| 1007 | Process Activity | `PROC:` | -| 2004 | Detection Finding | `FINDING:` | -| 5019 | Device Config State Change | `CONFIG:` | -| 6002 | Application Lifecycle | `LIFECYCLE:` | - -## Integration with External Tools - -The JSONL file can be shipped to any tool that accepts OCSF-formatted data: - -- **Splunk**: Use the [Splunk OCSF Add-on](https://splunkbase.splunk.com/app/6943) to ingest OCSF JSONL files. -- **Amazon Security Lake**: OCSF is the native schema for Security Lake. -- **Elastic**: Use Filebeat to ship JSONL files with the OCSF field mappings. -- **Custom pipelines**: Parse the JSONL file with `jq`, Python, or any JSON-capable tool. - -Example with `jq` to extract all denied connections: - -```console -$ cat /var/log/openshell-ocsf.2026-04-01.log | \ - jq -c 'select(.action == "Denied")' -``` - -## Relationship to Shorthand Logs - -The shorthand format in `openshell.YYYY-MM-DD.log` and the JSON format in `openshell-ocsf.YYYY-MM-DD.log` are derived from the same OCSF events. The shorthand is a human-readable projection; the JSON is the complete record. Both are generated at the same time from the same event data. - -The shorthand log is always active. The JSON export is opt-in via `ocsf_json_enabled`. - -## Next Steps - -- Learn how to [read the shorthand format](logging.md) for real-time monitoring. -- See the [OCSF specification](https://schema.ocsf.io/) for the full schema reference. diff --git a/fern/pages/observability/ocsf-json-export.mdx b/docs/observability/ocsf-json-export.mdx similarity index 97% rename from fern/pages/observability/ocsf-json-export.mdx rename to docs/observability/ocsf-json-export.mdx index 92e85730d..6612da6aa 100644 --- a/fern/pages/observability/ocsf-json-export.mdx +++ b/docs/observability/ocsf-json-export.mdx @@ -1,14 +1,10 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "OCSF JSON Export" sidebar-title: "OCSF JSON Export" description: "How to enable full OCSF JSON logging for SIEM integration, compliance, and structured analysis." keywords: "Generative AI, Cybersecurity, OCSF, JSON, SIEM, Compliance, Observability" -tags: - - OCSF - - JSON - - SIEM - - Compliance - - Observability --- The [shorthand log format](/observability/logging) is optimized for humans and agents reading logs in real time. For machine consumption, compliance archival, or SIEM integration, you can enable full OCSF JSON export. This writes every OCSF event as a complete JSON record in JSONL format, one JSON object per line. diff --git a/fern/pages/observability/overview.mdx b/docs/observability/overview.mdx similarity index 87% rename from fern/pages/observability/overview.mdx rename to docs/observability/overview.mdx index 0cef114b9..69fde7a06 100644 --- a/fern/pages/observability/overview.mdx +++ b/docs/observability/overview.mdx @@ -1,13 +1,10 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Observability" sidebar-title: "Overview" description: "Understand how OpenShell logs sandbox activity, how to access logs, and how to export structured OCSF records." keywords: "Generative AI, Cybersecurity, Logging, OCSF, Observability, Monitoring" -tags: - - Logging - - OCSF - - Observability - - Monitoring position: 1 --- diff --git a/docs/project.json b/docs/project.json deleted file mode 100644 index 96ae5a120..000000000 --- a/docs/project.json +++ /dev/null @@ -1 +0,0 @@ -{ "name": "openshell", "version": "latest" } diff --git a/docs/reference/default-policy.md b/docs/reference/default-policy.md deleted file mode 100644 index 27115aa85..000000000 --- a/docs/reference/default-policy.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: - page: "Default Policy Reference" - nav: "Default Policy" -description: "Breakdown of the built-in default policy applied when you create an OpenShell sandbox without a custom policy." -keywords: ["openshell default policy", "sandbox policy", "agent compatibility"] -topics: ["generative_ai", "cybersecurity"] -tags: ["ai_agents", "sandboxing", "security", "policy"] -content: - type: reference - difficulty: technical_beginner - audience: [engineer, data_scientist] ---- - -# Default Policy Reference - -The default policy is the policy applied when you create an OpenShell sandbox without `--policy`. It is baked into the community base image ([`ghcr.io/nvidia/openshell-community/sandboxes/base`](https://github.com/nvidia/openshell-community)) and defined in the community repo's `dev-sandbox-policy.yaml`. - -## Agent Compatibility - -The following table shows the coverage of the default policy for common agents. - -| Agent | Coverage | Action Required | -|---|---|---| -| Claude Code | Full | None. Works out of the box. | -| OpenCode | Partial | Add `opencode.ai` endpoint and OpenCode binary paths. | -| Codex | None | Provide a complete custom policy with OpenAI endpoints and Codex binary paths. | - -:::{important} -If you run a non-Claude agent without a custom policy, the agent's API calls are denied by the proxy. You must provide a policy that declares the agent's endpoints and binaries. -::: - -## Default Policy Blocks - -The default policy blocks are defined in the community base image. See the [openshell-community repository](https://github.com/nvidia/openshell-community) for the full `dev-sandbox-policy.yaml` source. diff --git a/fern/pages/reference/default-policy.mdx b/docs/reference/default-policy.mdx similarity index 90% rename from fern/pages/reference/default-policy.mdx rename to docs/reference/default-policy.mdx index d995b9b01..9f3a1d5cd 100644 --- a/fern/pages/reference/default-policy.mdx +++ b/docs/reference/default-policy.mdx @@ -1,13 +1,10 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Default Policy Reference" sidebar-title: "Default Policy" description: "Breakdown of the built-in default policy applied when you create an OpenShell sandbox without a custom policy." keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing, Security, Policy" -tags: - - AI Agents - - Sandboxing - - Security - - Policy position: 2 --- The default policy is the policy applied when you create an OpenShell sandbox without `--policy`. It is baked into the community base image ([`ghcr.io/nvidia/openshell-community/sandboxes/base`](https://github.com/nvidia/openshell-community)) and defined in the community repo's `dev-sandbox-policy.yaml`. diff --git a/docs/reference/gateway-auth.md b/docs/reference/gateway-auth.md deleted file mode 100644 index cc1cb0a7d..000000000 --- a/docs/reference/gateway-auth.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -title: - page: Gateway Authentication - nav: Gateway Authentication -description: Gateway resolution, authentication modes, connection flow, and credential file layout. -topics: -- Generative AI -- Cybersecurity -tags: -- Gateway -- Authentication -- mTLS -- Edge Authentication -- Reference -content: - type: reference - difficulty: technical_intermediate - audience: - - engineer ---- - - - -# Gateway Authentication - -This page describes how the CLI resolves a gateway, authenticates with it, and where credentials are stored. For how to deploy or register gateways, refer to {doc}`/sandboxes/manage-sandboxes`. - -## Gateway Resolution - -When any CLI command needs to talk to the gateway, it resolves the target through a priority chain: - -1. `--gateway-endpoint ` flag (direct URL). -2. `-g ` flag. -3. `OPENSHELL_GATEWAY` environment variable. -4. Active gateway from `~/.config/openshell/active_gateway`. - -The CLI loads gateway metadata from disk to determine the endpoint URL and authentication mode. - -## Authentication Modes - -The CLI uses one of three connection modes depending on the gateway's authentication configuration. - -### mTLS (local and remote gateways) - -The default mode for self-deployed gateways. When you run `gateway start` or `gateway add --local` / `gateway add --remote`, the CLI extracts mTLS certificates from the running container and stores them locally. Every subsequent request presents a client certificate to prove identity. - -The CLI loads three PEM files from `~/.config/openshell/gateways//mtls/`: - -| File | Purpose | -|---|---| -| `ca.crt` | CA certificate. Verifies the gateway's server certificate. | -| `tls.crt` | Client certificate. Proves the CLI's identity to the gateway. | -| `tls.key` | Client private key. | - -The connection flow: - -1. The CLI loads the three certificate files. -2. Opens a TCP connection to the gateway endpoint. -3. Performs a TLS handshake, presenting the client certificate. -4. The gateway verifies the client certificate against its CA. -5. An HTTP/2 channel is established. All CLI commands use this channel. - -### Edge JWT (cloud gateways) - -For gateways behind a reverse proxy that handles authentication (e.g. Cloudflare Access), the CLI uses a browser-based login flow and routes traffic through a WebSocket tunnel. - -**Registration flow** (`openshell gateway add https://gateway.example.com`): - -1. The CLI stores gateway metadata with the edge authentication mode. -2. Opens your browser to the gateway's authentication endpoint. -3. The reverse proxy handles login (SSO, identity provider, etc.). -4. After authentication, the browser relays the authorization token back to the CLI via a localhost callback. -5. The CLI stores the token and sets the gateway as active. - -**Connection flow** (subsequent commands): - -1. The CLI starts a local proxy that listens on an ephemeral port. -2. The proxy opens a WebSocket connection (`wss://`) to the gateway, attaching the stored bearer token in the upgrade headers. -3. The reverse proxy authenticates the WebSocket upgrade request. -4. The gateway bridges the WebSocket into the same service that handles direct mTLS connections. -5. CLI commands send requests through the local proxy as plaintext HTTP/2 over the tunnel. - -This is transparent to the user. All CLI commands work the same regardless of whether the gateway uses mTLS or edge authentication. - -**Re-authentication**: If the token expires, run `openshell gateway login` to open the browser flow again and update the stored token. - -### Plaintext - -When a gateway is deployed with `--plaintext`, TLS is disabled entirely. The CLI connects over plain HTTP/2. This mode is intended for gateways behind a trusted reverse proxy or tunnel that handles TLS termination externally. - -## File Layout - -All gateway credentials and metadata are stored under `~/.config/openshell/`: - -``` -openshell/ - active_gateway # Plain text: active gateway name - gateways/ - / - metadata.json # Gateway metadata (endpoint, auth mode, type) - mtls/ # mTLS bundle (local and remote gateways) - ca.crt # CA certificate - tls.crt # Client certificate - tls.key # Client private key - edge_token # Edge auth JWT (cloud gateways) - last_sandbox # Last-used sandbox for this gateway -``` diff --git a/fern/pages/reference/gateway-auth.mdx b/docs/reference/gateway-auth.mdx similarity index 94% rename from fern/pages/reference/gateway-auth.mdx rename to docs/reference/gateway-auth.mdx index 85d72b552..4428dcc89 100644 --- a/fern/pages/reference/gateway-auth.mdx +++ b/docs/reference/gateway-auth.mdx @@ -1,17 +1,11 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Gateway Authentication" description: "Gateway resolution, authentication modes, connection flow, and credential file layout." keywords: "Generative AI, Cybersecurity, Gateway, Authentication, mTLS, Edge Authentication, Reference" -tags: - - Gateway - - Authentication - - mTLS - - Edge Authentication - - Reference position: 1 --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} This page describes how the CLI resolves a gateway, authenticates with it, and where credentials are stored. For how to deploy or register gateways, refer to [Manage Sandboxes](/sandboxes/manage-sandboxes). diff --git a/docs/reference/policy-schema.md b/docs/reference/policy-schema.md deleted file mode 100644 index 7ad317f36..000000000 --- a/docs/reference/policy-schema.md +++ /dev/null @@ -1,252 +0,0 @@ ---- -title: - page: Policy Schema Reference - nav: Policy Schema -description: Complete field reference for the sandbox policy YAML including static and dynamic sections. -topics: -- Generative AI -- Cybersecurity -tags: -- Policy -- Schema -- YAML -- Reference -- Security -content: - type: reference - difficulty: technical_advanced - audience: - - engineer ---- - - - -# Policy Schema Reference - -Complete field reference for the sandbox policy YAML. Each field is documented with its type, whether it is required, and whether it is static (locked at sandbox creation) or dynamic (hot-reloadable on a running sandbox). - -## Top-Level Structure - -A policy YAML file contains the following top-level fields: - -```yaml -version: 1 -filesystem_policy: { ... } -landlock: { ... } -process: { ... } -network_policies: { ... } -``` - -| Field | Type | Required | Category | Description | -|---|---|---|---|---| -| `version` | integer | Yes | -- | Policy schema version. Must be `1`. | -| `filesystem_policy` | object | No | Static | Controls which directories the agent can read and write. | -| `landlock` | object | No | Static | Configures Landlock LSM enforcement behavior. | -| `process` | object | No | Static | Sets the user and group the agent process runs as. | -| `network_policies` | map | No | Dynamic | Declares which binaries can reach which network endpoints. | - -Static fields are set at sandbox creation time. Changing them requires destroying and recreating the sandbox. Dynamic fields can be updated on a running sandbox with `openshell policy set` and take effect without restarting. - -## Version - -The version field identifies which schema the policy uses: - -| Field | Type | Required | Description | -|---|---|---|---| -| `version` | integer | Yes | Schema version number. Currently must be `1`. | - -## Filesystem Policy - -**Category:** Static - -Controls filesystem access inside the sandbox. Paths not listed in either `read_only` or `read_write` are inaccessible. - -| Field | Type | Required | Description | -|---|---|---|---| -| `include_workdir` | bool | No | When `true`, automatically adds the agent's working directory to `read_write`. | -| `read_only` | list of strings | No | Paths the agent can read but not modify. Typically system directories like `/usr`, `/lib`, `/etc`. | -| `read_write` | list of strings | No | Paths the agent can read and write. Typically `/sandbox` (working directory) and `/tmp`. | - -**Validation constraints:** - -- Every path must be absolute (start with `/`). -- Paths must not contain `..` traversal components. The server normalizes paths before storage, but rejects policies where traversal would escape the intended scope. -- Read-write paths must not be overly broad (for example, `/` alone is rejected). -- Each individual path must not exceed 4096 characters. -- The combined total of `read_only` and `read_write` paths must not exceed 256. - -Policies that violate these constraints are rejected with `INVALID_ARGUMENT` at creation or update time. Disk-loaded YAML policies that fail validation fall back to a restrictive default. - -Example: - -```yaml -filesystem_policy: - include_workdir: true - read_only: - - /usr - - /lib - - /proc - - /dev/urandom - - /etc - read_write: - - /sandbox - - /tmp - - /dev/null -``` - -## Landlock - -**Category:** Static - -Configures [Landlock LSM](https://docs.kernel.org/security/landlock.html) enforcement at the kernel level. Landlock provides mandatory filesystem access control below what UNIX permissions allow. - -| Field | Type | Required | Values | Description | -|---|---|---|---|---| -| `compatibility` | string | No | `best_effort`, `hard_requirement` | How OpenShell handles Landlock failures. See behavior table below. | - -**Compatibility modes:** - -| Value | Kernel ABI unavailable | Individual path inaccessible | All paths inaccessible | -|---|---|---|---| -| `best_effort` | Warns and continues without Landlock. | Skips the path, applies remaining rules. | Warns and continues without Landlock (refuses to apply an empty ruleset). | -| `hard_requirement` | Aborts sandbox startup. | Aborts sandbox startup. | Aborts sandbox startup. | - -`best_effort` (the default) is appropriate for most deployments. It handles missing paths gracefully -- for example, `/app` may not exist in every container image but is included in the baseline path set for containers that do have it. Individual missing paths are skipped while the remaining filesystem rules are still enforced. - -`hard_requirement` is for environments where any gap in filesystem isolation is unacceptable. If a listed path cannot be opened for any reason (missing, permission denied, symlink loop), sandbox startup fails immediately rather than running with reduced protection. - -When a path is skipped under `best_effort`, the sandbox logs a warning that includes the path, the specific error, and a human-readable reason (for example, "path does not exist" or "permission denied"). - -Example: - -```yaml -landlock: - compatibility: best_effort -``` - -## Process - -**Category:** Static - -Sets the OS-level identity for the agent process inside the sandbox. - -| Field | Type | Required | Description | -|---|---|---|---| -| `run_as_user` | string | No | The user name or UID the agent process runs as. Default: `sandbox`. | -| `run_as_group` | string | No | The group name or GID the agent process runs as. Default: `sandbox`. | - -**Validation constraint:** Neither `run_as_user` nor `run_as_group` may be set to `root` or `0`. Policies that request root process identity are rejected at creation or update time. - -Example: - -```yaml -process: - run_as_user: sandbox - run_as_group: sandbox -``` - -## Network Policies - -**Category:** Dynamic - -A map of named network policy entries. Each entry declares a set of endpoints and a set of binaries. Only the listed binaries are permitted to connect to the listed endpoints. The map key is a logical identifier. The `name` field inside the entry is the display name used in logs. - -### Network Policy Entry - -Each entry in the `network_policies` map has the following fields: - -| Field | Type | Required | Description | -|---|---|---|---| -| `name` | string | No | Display name for the policy entry. Used in log output. Defaults to the map key. | -| `endpoints` | list of endpoint objects | Yes | Hosts and ports this entry permits. | -| `binaries` | list of binary objects | Yes | Executables allowed to connect to these endpoints. | - -### Endpoint Object - -Each endpoint defines a reachable destination and optional inspection rules. - -| Field | Type | Required | Description | -|---|---|---|---| -| `host` | string | Yes | Hostname or IP address. Supports wildcards: `*.example.com` matches any subdomain. | -| `port` | integer | Yes | TCP port number. | -| `protocol` | string | No | Set to `rest` to enable HTTP request inspection. Omit for TCP passthrough. | -| `tls` | string | No | TLS handling mode. The proxy auto-detects TLS by peeking the first bytes of each connection and terminates it when `protocol` is `rest`, so this field is optional in most cases. Set to `skip` to disable auto-detection for edge cases such as client-certificate mTLS or non-standard protocols. The values `terminate` and `passthrough` are deprecated and log a warning; they are still accepted for backward compatibility but have no effect on behavior. | -| `enforcement` | string | No | `enforce` actively blocks disallowed requests. `audit` logs violations but allows traffic through. | -| `access` | string | No | HTTP access level. One of `read-only`, `read-write`, or `full`. Mutually exclusive with `rules`. | -| `rules` | list of rule objects | No | Fine-grained per-method, per-path allow rules. Mutually exclusive with `access`. | - -#### Access Levels - -The `access` field accepts one of the following values: - -| Value | Allowed HTTP Methods | -|---|---| -| `full` | All methods and paths. | -| `read-only` | `GET`, `HEAD`, `OPTIONS`. | -| `read-write` | `GET`, `HEAD`, `OPTIONS`, `POST`, `PUT`, `PATCH`. | - -#### Rule Object - -Used when `access` is not set. Each rule explicitly allows a method and path combination. - -| Field | Type | Required | Description | -|---|---|---|---| -| `allow.method` | string | Yes | HTTP method to allow (for example, `GET`, `POST`). | -| `allow.path` | string | Yes | URL path pattern. Supports `*` and `**` glob syntax. | -| `allow.query` | map | No | Query parameter matchers keyed by decoded param name. Matcher value can be a glob string (`tag: "foo-*"`) or an object with `any` (`tag: { any: ["foo-*", "bar-*"] }`). | - -Example with rules: - -```yaml -rules: - - allow: - method: GET - path: /**/info/refs* - query: - service: "git-*" - - allow: - method: POST - path: /**/git-upload-pack - query: - tag: - any: ["v1.*", "v2.*"] -``` - -### Binary Object - -Identifies an executable that is permitted to use the associated endpoints. - -| Field | Type | Required | Description | -|---|---|---|---| -| `path` | string | Yes | Filesystem path to the executable. Supports glob patterns with `*` and `**`. For example, `/sandbox/.vscode-server/**` matches any executable under that directory tree. | - -### Full Example - -The following policy grants read-only GitHub API access and npm registry access: - -```yaml -network_policies: - github_rest_api: - name: github-rest-api - endpoints: - - host: api.github.com - port: 443 - protocol: rest - enforcement: enforce - access: read-only - binaries: - - path: /usr/local/bin/claude - - path: /usr/bin/node - - path: /usr/bin/gh - npm_registry: - name: npm-registry - endpoints: - - host: registry.npmjs.org - port: 443 - binaries: - - path: /usr/bin/npm - - path: /usr/bin/node -``` diff --git a/fern/pages/reference/policy-schema.mdx b/docs/reference/policy-schema.mdx similarity index 97% rename from fern/pages/reference/policy-schema.mdx rename to docs/reference/policy-schema.mdx index e958b99f9..4a5b3b384 100644 --- a/fern/pages/reference/policy-schema.mdx +++ b/docs/reference/policy-schema.mdx @@ -1,18 +1,12 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Policy Schema Reference" sidebar-title: "Policy Schema" description: "Complete field reference for the sandbox policy YAML including static and dynamic sections." keywords: "Generative AI, Cybersecurity, Policy, Schema, YAML, Reference, Security" -tags: - - Policy - - Schema - - YAML - - Reference - - Security position: 3 --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} Complete field reference for the sandbox policy YAML. Each field is documented with its type, whether it is required, and whether it is static (locked at sandbox creation) or dynamic (hot-reloadable on a running sandbox). diff --git a/docs/reference/support-matrix.md b/docs/reference/support-matrix.md deleted file mode 100644 index 96ece5859..000000000 --- a/docs/reference/support-matrix.md +++ /dev/null @@ -1,66 +0,0 @@ - - -# Support Matrix - -This page lists the platform, software, runtime, and kernel requirements for running OpenShell. - -## Supported Platforms - -OpenShell publishes multi-architecture container images for `linux/amd64` and `linux/arm64`. The CLI is supported on the following host platforms: - -| Platform | Architecture | Status | -| -------------------------------- | --------------------- | --------- | -| Linux (Debian/Ubuntu) | x86_64 (amd64) | Supported | -| Linux (Debian/Ubuntu) | aarch64 (arm64) | Supported | -| macOS (Docker Desktop) | Apple Silicon (arm64) | Supported | -| Windows (WSL 2 + Docker Desktop) | x86_64 | Experimental | - -## Software Prerequisites - -The following software must be installed on the host before using the OpenShell CLI: - -| Component | Minimum Version | Notes | -| ------------------------------- | --------------- | ----------------------------------------------- | -| Docker Desktop or Docker Engine | 28.04 | Must be running before any `openshell` command. | - -## Sandbox Runtime Versions - -Sandbox container images are maintained in the [openshell-community](https://github.com/nvidia/openshell-community) repository. Refer to that repository for the current list of installed components and their versions. - -## Container Images - -OpenShell publishes two container images. Both are published for `linux/amd64` and `linux/arm64`. - -| Image | Reference | Pulled When | -| ------- | ----------------------------------------- | -------------------------------- | -| Cluster | `ghcr.io/nvidia/openshell/cluster:latest` | `openshell gateway start` | -| Gateway | `ghcr.io/nvidia/openshell/gateway:latest` | Cluster startup (via Helm chart) | - -The cluster image bundles the Helm charts, Kubernetes manifests, and the `openshell-sandbox` supervisor binary required to bootstrap the control plane. The supervisor binary is side-loaded into sandbox pods at runtime through a read-only host volume mount. The gateway image is pulled at cluster startup and runs the API server. - -Sandbox images are maintained separately in the [openshell-community](https://github.com/nvidia/openshell-community) repository. - -To override the default image references, set the following environment variables: - -| Variable | Purpose | -| ------------------------------ | --------------------------------------------------- | -| `OPENSHELL_CLUSTER_IMAGE` | Override the cluster image reference. | -| `OPENSHELL_COMMUNITY_REGISTRY` | Override the registry for community sandbox images. | - -## Kernel Requirements - -OpenShell enforces sandbox isolation through two Linux kernel security modules: - -| Module | Requirement | Details | -| -------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [Landlock LSM](https://docs.kernel.org/security/landlock.html) | Recommended | Enforces filesystem access restrictions at the kernel level. The `best_effort` compatibility mode uses the highest Landlock ABI the host kernel supports. The `hard_requirement` mode fails sandbox creation if the required ABI is unavailable. | -| seccomp | Required | Filters dangerous system calls. Available on all modern Linux kernels (3.17+). | - -On macOS, these kernel modules run inside the Docker Desktop Linux VM, not on the host kernel. - -## Agent Compatibility - -For the full list of supported agents and their default policy coverage, refer to the {doc}`../about/supported-agents` page. diff --git a/fern/pages/reference/support-matrix.mdx b/docs/reference/support-matrix.mdx similarity index 96% rename from fern/pages/reference/support-matrix.mdx rename to docs/reference/support-matrix.mdx index 146d048fc..744fba53c 100644 --- a/fern/pages/reference/support-matrix.mdx +++ b/docs/reference/support-matrix.mdx @@ -1,10 +1,10 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Support Matrix" description: "" position: 4 --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} This page lists the platform, software, runtime, and kernel requirements for running OpenShell. diff --git a/docs/resources/license.md b/docs/resources/license.md deleted file mode 100644 index c452ac74a..000000000 --- a/docs/resources/license.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: - page: License - nav: License -description: NVIDIA OpenShell is licensed under the Apache License, Version 2.0. -topics: -- Legal -tags: -- License -- Apache 2.0 -content: - type: reference - difficulty: technical_beginner - audience: - - engineer - - data_scientist ---- - - - -# License - -NVIDIA OpenShell is licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). - -```{include} ../../LICENSE -``` diff --git a/fern/pages/resources/license.mdx b/docs/resources/license.mdx similarity index 98% rename from fern/pages/resources/license.mdx rename to docs/resources/license.mdx index 5db1228aa..56281bea1 100644 --- a/fern/pages/resources/license.mdx +++ b/docs/resources/license.mdx @@ -1,14 +1,11 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "License" description: "NVIDIA OpenShell is licensed under the Apache License, Version 2.0." keywords: "Legal, License, Apache 2.0" -tags: - - License - - Apache 2.0 position: 1 --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} NVIDIA OpenShell is licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/fern/pages/sandboxes/about.mdx b/docs/sandboxes/about.mdx similarity index 94% rename from fern/pages/sandboxes/about.mdx rename to docs/sandboxes/about.mdx index 1f4a21923..92f4e20fe 100644 --- a/fern/pages/sandboxes/about.mdx +++ b/docs/sandboxes/about.mdx @@ -1,19 +1,12 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "About Gateways and Sandboxes" sidebar-title: "About Gateways and Sandboxes" description: "Understand gateways, gateway types, sandbox lifecycle, supported agents, built-in default policy, and network access rules in OpenShell." keywords: "Generative AI, Cybersecurity, Gateway, Sandboxing, AI Agents, Security, Policy, Isolation" -tags: - - Gateway - - Sandboxing - - AI Agents - - Security - - Policy - - Isolation position: 1 --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} Every OpenShell deployment starts with a *gateway* and one or more *sandboxes*. The gateway is the control plane that manages sandbox lifecycle, providers, and policies. A sandbox is the data plane, a safe, private execution environment where an AI agent runs. Each sandbox runs with multiple layers of protection that prevent unauthorized data access, credential exposure, and network exfiltration. Protection layers include filesystem restrictions (Landlock), system call filtering (seccomp), network namespace isolation, and a privacy-enforcing HTTP CONNECT proxy. diff --git a/docs/sandboxes/community-sandboxes.md b/docs/sandboxes/community-sandboxes.md deleted file mode 100644 index 0e3df84bd..000000000 --- a/docs/sandboxes/community-sandboxes.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -title: - page: Community Sandboxes - nav: Community Sandboxes -description: Use pre-built sandboxes from the OpenShell Community catalog or contribute your own. -topics: -- Generative AI -- Cybersecurity -tags: -- Community -- Sandbox -- Container Image -- Open Source -content: - type: concept - difficulty: technical_beginner - audience: - - engineer - - data_scientist ---- - - - -# Community Sandboxes - -Use pre-built sandboxes from the OpenShell Community catalog, or contribute your -own. - -## What Are Community Sandboxes - -Community sandboxes are ready-to-use environments published in the -[OpenShell Community](https://github.com/NVIDIA/OpenShell-Community) repository. -Each sandbox bundles a Dockerfile, policy, optional skills, and startup scripts -into a single package that you can launch with one command. - -## Current Catalog - -The following community sandboxes are available in the catalog. - -| Sandbox | Description | -|---|---| -| `base` | Foundational image with system tools and dev environment | -| `ollama` | Ollama with cloud and local model support, Claude Code, OpenCode, and Codex pre-installed. Use `ollama launch` inside the sandbox to start coding agents with zero config. -| `openclaw` | Open agent manipulation and control | -| `sdg` | Synthetic data generation workflows | - -## Use a Community Sandbox - -Launch a community sandbox by name with the `--from` flag: - -```console -$ openshell sandbox create --from openclaw -``` - -When you pass `--from` with a community sandbox name, the CLI: - -1. Resolves the name against the - [OpenShell Community](https://github.com/NVIDIA/OpenShell-Community) repository. -2. Pulls the Dockerfile, policy, skills, and any startup scripts. -3. Builds the container image locally. -4. Creates the sandbox with the bundled configuration applied. - -You end up with a running sandbox whose image, policy, and tooling are all -preconfigured by the community package. - -### Other Sources - -The `--from` flag also accepts: - -- Local directory paths: Point to a directory on disk that contains a - Dockerfile and optional policy/skills: - - ```console - $ openshell sandbox create --from ./my-sandbox-dir - ``` - -- Container image references: Use an existing container image directly: - - ```console - $ openshell sandbox create --from my-registry.example.com/my-image:latest - ``` - -## Contribute a Community Sandbox - -Each community sandbox is a directory under `sandboxes/` in the -[OpenShell Community](https://github.com/NVIDIA/OpenShell-Community) repository. -At minimum, a sandbox directory must contain the following files: - -- `Dockerfile` that defines the container image. -- `README.md` that describes the sandbox and how to use it. - -You can also include the following optional files: - -- `policy.yaml` that defines the default policy applied when the sandbox launches. -- `skills/` that contains agent skill definitions bundled with the sandbox. -- Startup scripts that are any scripts the Dockerfile or entrypoint invokes. - -To contribute, fork the repository, add your sandbox directory, and open a pull -request. Refer to the repository's -[CONTRIBUTING.md](https://github.com/NVIDIA/OpenShell-Community/blob/main/CONTRIBUTING.md) -for submission guidelines. - -:::{note} -The community catalog is designed to grow. If you have built a sandbox that -supports a particular workflow (data processing, simulation, code review, -or anything else), consider contributing it back so others can use it. -::: - -## Next Steps - -Explore related topics: - -- **Need to supply API keys or tokens?** Set up {doc}`manage-providers` for credential management. -- **Want to customize the sandbox policy?** Write custom rules in {doc}`policies`. diff --git a/fern/pages/sandboxes/community-sandboxes.mdx b/docs/sandboxes/community-sandboxes.mdx similarity index 94% rename from fern/pages/sandboxes/community-sandboxes.mdx rename to docs/sandboxes/community-sandboxes.mdx index 68e0398e0..be640d7b6 100644 --- a/fern/pages/sandboxes/community-sandboxes.mdx +++ b/docs/sandboxes/community-sandboxes.mdx @@ -1,16 +1,11 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Community Sandboxes" description: "Use pre-built sandboxes from the OpenShell Community catalog or contribute your own." keywords: "Generative AI, Cybersecurity, Community, Sandbox, Container Image, Open Source" -tags: - - Community - - Sandbox - - Container Image - - Open Source position: 6 --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} Use pre-built sandboxes from the OpenShell Community catalog, or contribute your own. diff --git a/docs/sandboxes/index.md b/docs/sandboxes/index.md deleted file mode 100644 index 59f2c0781..000000000 --- a/docs/sandboxes/index.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -title: - page: About Gateways and Sandboxes - nav: Gateways and Sandboxes -description: Understand gateways, gateway types, sandbox lifecycle, supported agents, built-in default policy, and network access rules in OpenShell. -topics: -- Generative AI -- Cybersecurity -tags: -- Gateway -- Sandboxing -- AI Agents -- Security -- Policy -- Isolation -content: - type: concept - difficulty: technical_beginner - audience: - - engineer - - data_scientist ---- - - - -# About Gateways and Sandboxes - -Every OpenShell deployment starts with a *gateway* and one or more *sandboxes*. The gateway is the control plane that manages sandbox lifecycle, providers, and policies. A sandbox is the data plane, a safe, private execution environment where an AI agent runs. Each sandbox runs with multiple layers of protection that prevent unauthorized data access, credential exposure, and network exfiltration. Protection layers include filesystem restrictions (Landlock), system call filtering (seccomp), network namespace isolation, and a privacy-enforcing HTTP CONNECT proxy. - -## Gateway Types - -A gateway provisions sandboxes, brokers CLI requests, enforces policies, and manages provider credentials. OpenShell supports three deployment models, so the gateway can run wherever your workload requires. - -| Type | Where It Runs | Best For | -|---|---|---| -| **Local** | Docker on your workstation | Solo development and quick iteration. The CLI auto-bootstraps a local gateway if none exists. | -| **Remote** | Docker on a remote host via SSH | Running sandboxes on a more powerful machine (for example, a DGX Spark) while keeping the CLI on your laptop. | -| **Cloud** | Behind a reverse proxy (for example, Cloudflare Access) | Individual users accessing OpenShell behind a cloud VM. Cloud gateways are not yet intended for shared team access. | - -All three types expose the same API surface. Sandboxes, policies, and providers work identically regardless of where the gateway runs. The only difference is how the CLI reaches the gateway, whether through a direct Docker socket, SSH tunnel, or HTTPS through a proxy. - -:::{tip} -You do not need to deploy a gateway manually. Running `openshell sandbox create` without a gateway auto-bootstraps a local one for you. -::: - -## Sandbox Lifecycle - -Every sandbox moves through a defined set of phases: - -| Phase | Description | -|---|---| -| Provisioning | The runtime is setting up the sandbox environment, injecting credentials, and applying your policy. | -| Ready | The sandbox is running. The agent process is active and all isolation layers are enforced. You can connect, sync files, and view logs. | -| Error | Something went wrong during provisioning or execution. Check logs with `openshell logs` for details. | -| Deleting | The sandbox is being torn down. The system releases resources and purges credentials. | - -## Sandbox Policies - -OpenShell ships a built-in policy that covers common agent workflows out of the box. -When you create a sandbox without `--policy`, the default policy is applied. It controls three areas. - -| Layer | What It Controls | How It Works | -|---|---|---| -| Filesystem | What the agent can access on disk | Paths are split into read-only and read-write sets. [Landlock LSM](https://docs.kernel.org/security/landlock.html) enforces these restrictions at the kernel level. | -| Network | What the agent can reach on the network | Each policy block pairs allowed destinations (host and port) with allowed binaries (executable paths). The proxy matches every outbound connection to the binary that opened it. Both must match or the connection is denied. | -| Process | What privileges the agent has | The agent runs as an unprivileged user with seccomp filters that block dangerous system calls. No `sudo`, no `setuid`, no path to elevated privileges. | - -For the full breakdown of each default policy block and agent compatibility details, refer to {doc}`../reference/default-policy`. - -For the full policy structure with annotated YAML examples, refer to {doc}`policies`. - -## Next Steps - -Continue with one of the following: - -- To create your first sandbox, refer to {doc}`manage-sandboxes`. -- To supply API keys or tokens, refer to {doc}`manage-providers`. -- To control what the agent can access, refer to {doc}`policies`. -- To use a pre-built environment, refer to the {doc}`community-sandboxes` catalog. diff --git a/docs/sandboxes/manage-gateways.md b/docs/sandboxes/manage-gateways.md deleted file mode 100644 index d4d9ccf55..000000000 --- a/docs/sandboxes/manage-gateways.md +++ /dev/null @@ -1,232 +0,0 @@ ---- -title: - page: Deploy and Manage Gateways - nav: Gateways -description: Deploy local and remote gateways, register cloud gateways, and manage multiple gateway environments. -topics: -- Generative AI -- Cybersecurity -tags: -- Gateway -- Deployment -- Remote Gateway -- CLI -content: - type: how_to - difficulty: technical_beginner - audience: - - engineer - - data_scientist ---- - - - -# Deploy and Manage Gateways - -The gateway is the control plane for OpenShell. All control-plane traffic between the CLI and running sandboxes flows through the gateway. - -The gateway is responsible for: - -- Provisioning and managing sandboxes, including creation, deletion, and status monitoring. -- Storing provider credentials (API keys, tokens) and delivering them to sandboxes at startup. -- Delivering network and filesystem policies to sandboxes. Policy enforcement itself happens inside each sandbox through the proxy, OPA, Landlock, and seccomp. -- Managing inference configuration and serving inference bundles so sandboxes can route requests to the correct backend. -- Providing the SSH tunnel endpoint so you can connect to sandboxes without exposing them directly. - -The gateway runs inside a Docker container and exposes a single port (gRPC and HTTP multiplexed), secured by mTLS by default. No separate Kubernetes installation is required. It can be deployed locally, on a remote host via SSH, or behind a cloud reverse proxy. - -## Deploy a Local Gateway - -Deploy a gateway on your workstation. The only prerequisite is a running Docker daemon. - -```console -$ openshell gateway start -``` - -The gateway becomes reachable at `https://127.0.0.1:8080`. Verify it is healthy: - -```console -$ openshell status -``` - -:::{tip} -You do not need to deploy a gateway manually. If you run `openshell sandbox create` without a gateway, the CLI auto-bootstraps a local gateway for you. -::: - -To use a different port or name: - -```console -$ openshell gateway start --port 9090 -$ openshell gateway start --name dev-local -``` - -## Deploy a Remote Gateway - -Deploy a gateway on a remote machine accessible via SSH. The only dependency on the remote host is Docker. - -```console -$ openshell gateway start --remote user@hostname -``` - -The gateway is reachable at `https://:8080`. - -To specify an SSH key: - -```console -$ openshell gateway start --remote user@hostname --ssh-key ~/.ssh/my_key -``` - -:::{note} -For DGX Spark, use your Spark's mDNS hostname: - -```console -$ openshell gateway start --remote @.local -``` -::: - -## Register an Existing Gateway - -Use `openshell gateway add` to register a gateway that is already running. - -### Cloud Gateway - -Register a gateway behind a reverse proxy such as Cloudflare Access: - -```console -$ openshell gateway add https://gateway.example.com -``` - -This opens your browser for the proxy's login flow. After authentication, the CLI stores a bearer token and sets the gateway as active. - -To give the gateway a specific name instead of deriving it from the hostname, use `--name`: - -```console -$ openshell gateway add https://gateway.example.com --name production -``` - -If the token expires later, re-authenticate with: - -```console -$ openshell gateway login -``` - -### Remote Gateway - -Register a gateway on a remote host you have SSH access to: - -```console -$ openshell gateway add https://remote-host:8080 --remote user@remote-host -``` - -Or use the `ssh://` scheme to combine the SSH destination and gateway port: - -```console -$ openshell gateway add ssh://user@remote-host:8080 -``` - -### Local Gateway - -Register a gateway running locally that was started outside the CLI: - -```console -$ openshell gateway add https://127.0.0.1:8080 --local -``` - -## Manage Multiple Gateways - -One gateway is always the active gateway. All CLI commands target it by default. Both `gateway start` and `gateway add` automatically set the new gateway as active. - -List all registered gateways: - -```console -$ openshell gateway select -``` - -Switch the active gateway: - -```console -$ openshell gateway select my-remote-cluster -``` - -Override the active gateway for a single command with `-g`: - -```console -$ openshell status -g my-other-cluster -``` - -Show deployment details for a gateway, including endpoint, auth mode, and port: - -```console -$ openshell gateway info -$ openshell gateway info --name my-remote-cluster -``` - -## Advanced Start Options - -| Flag | Purpose | -|---|---| -| `--gpu` | Enable NVIDIA GPU passthrough. Requires NVIDIA drivers and the Container Toolkit on the host. OpenShell auto-selects CDI when enabled on the daemon and falls back to Docker's NVIDIA GPU request path (`--gpus all`) otherwise. | -| `--plaintext` | Listen on HTTP instead of mTLS. Use behind a TLS-terminating reverse proxy. | -| `--disable-gateway-auth` | Skip mTLS client certificate checks. Use when a reverse proxy cannot forward client certs. | -| `--registry-username` | Username for registry authentication. Defaults to `__token__` when `--registry-token` is set. Only needed for private registries. Also configurable with `OPENSHELL_REGISTRY_USERNAME`. | -| `--registry-token` | Authentication token for pulling container images. For GHCR, a GitHub PAT with `read:packages` scope. Only needed for private registries. Also configurable with `OPENSHELL_REGISTRY_TOKEN`. | - -## Stop and Destroy - -Stop a gateway while preserving its state for later restart: - -```console -$ openshell gateway stop -``` - -Permanently destroy a gateway and all its state: - -```console -$ openshell gateway destroy -``` - -For cloud gateways, `gateway destroy` removes only the local registration. It does not affect the remote deployment. - -Target a specific gateway with `--name`: - -```console -$ openshell gateway stop --name my-gateway -$ openshell gateway destroy --name my-gateway -``` - -## Troubleshoot - -Check gateway health: - -```console -$ openshell status -``` - -View gateway logs: - -```console -$ openshell doctor logs -$ openshell doctor logs --tail # stream live -$ openshell doctor logs --lines 50 # last 50 lines -``` - -Run a command inside the gateway container for deeper inspection: - -```console -$ openshell doctor exec -- kubectl get pods -A -$ openshell doctor exec -- sh -``` - -If the gateway is in a bad state, recreate it: - -```console -$ openshell gateway start --recreate -``` - -## Next Steps - -- To create a sandbox using the gateway, refer to {doc}`manage-sandboxes`. -- To install the CLI and get started quickly, refer to the {doc}`/get-started/quickstart`. diff --git a/fern/pages/sandboxes/manage-gateways.mdx b/docs/sandboxes/manage-gateways.mdx similarity index 96% rename from fern/pages/sandboxes/manage-gateways.mdx rename to docs/sandboxes/manage-gateways.mdx index ab2eebfa9..84dfcc5be 100644 --- a/fern/pages/sandboxes/manage-gateways.mdx +++ b/docs/sandboxes/manage-gateways.mdx @@ -1,17 +1,12 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Deploy and Manage Gateways" sidebar-title: "Gateways" description: "Deploy local and remote gateways, register cloud gateways, and manage multiple gateway environments." keywords: "Generative AI, Cybersecurity, Gateway, Deployment, Remote Gateway, CLI" -tags: - - Gateway - - Deployment - - Remote Gateway - - CLI position: 3 --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} The gateway is the control plane for OpenShell. All control-plane traffic between the CLI and running sandboxes flows through the gateway. diff --git a/docs/sandboxes/manage-providers.md b/docs/sandboxes/manage-providers.md deleted file mode 100644 index 6d35766bf..000000000 --- a/docs/sandboxes/manage-providers.md +++ /dev/null @@ -1,211 +0,0 @@ ---- -title: - page: Providers - nav: Providers -description: Create and manage credential providers that inject API keys and tokens into OpenShell sandboxes. -topics: -- Generative AI -- Cybersecurity -tags: -- Providers -- Credentials -- API Keys -- Sandbox -- Security -content: - type: how_to - difficulty: technical_beginner - audience: - - engineer - - data_scientist ---- - - - -# Manage Providers and Credentials - -AI agents typically need credentials to access external services: an API key for the AI model provider, a token for GitHub or GitLab, and so on. OpenShell manages these credentials as first-class entities called *providers*. - -Create and manage providers that supply credentials to sandboxes. - -## Create a Provider - -Providers can be created from local environment variables or with explicit credential values. - -### From Local Credentials - -The fastest way to create a provider is to let the CLI discover credentials from -your shell environment: - -```console -$ openshell provider create --name my-claude --type claude --from-existing -``` - -This reads `ANTHROPIC_API_KEY` or `CLAUDE_API_KEY` from your current environment -and stores them in the provider. - -### With Explicit Credentials - -Supply a credential value directly: - -```console -$ openshell provider create --name my-api --type generic --credential API_KEY=sk-abc123 -``` - -### Bare Key Form - -Pass a key name without a value to read the value from the environment variable -of that name: - -```console -$ openshell provider create --name my-api --type generic --credential API_KEY -``` - -This looks up the current value of `$API_KEY` in your shell and stores it. - -## Manage Providers - -List, inspect, update, and delete providers from the active cluster. - -List all providers: - -```console -$ openshell provider list -``` - -Inspect a provider: - -```console -$ openshell provider get my-claude -``` - -Update a provider's credentials: - -```console -$ openshell provider update my-claude --type claude --from-existing -``` - -Delete a provider: - -```console -$ openshell provider delete my-claude -``` - -## Attach Providers to Sandboxes - -Pass one or more `--provider` flags when creating a sandbox: - -```console -$ openshell sandbox create --provider my-claude --provider my-github -- claude -``` - -Each `--provider` flag attaches one provider. The sandbox receives all -credentials from every attached provider at runtime. - -:::{warning} -Providers cannot be added to a running sandbox. If you need to attach an -additional provider, delete the sandbox and recreate it with all required -providers specified. -::: - -### Auto-Discovery Shortcut - -When the trailing command in `openshell sandbox create` is a recognized tool name (`claude`, `codex`, or `opencode`), the CLI auto-creates the required -provider from your local credentials if one does not already exist. You do not -need to create the provider separately: - -```console -$ openshell sandbox create -- claude -``` - -This detects `claude` as a known tool, finds your `ANTHROPIC_API_KEY`, creates -a provider, attaches it to the sandbox, and launches Claude Code. - -## How Credential Injection Works - -The agent process inside the sandbox never sees real credential values. At startup, the proxy replaces each credential with an opaque placeholder token in the agent's environment. When the agent sends an HTTP request containing a placeholder, the proxy resolves it to the real credential before forwarding upstream. - -This resolution requires the proxy to see plaintext HTTP. Endpoints must use `protocol: rest` in the policy (which auto-terminates TLS) or explicit `tls: terminate`. Endpoints without TLS termination pass traffic through as an opaque stream, and credential placeholders are forwarded unresolved. - -### Supported injection locations - -The proxy resolves credential placeholders in the following parts of an HTTP request: - -| Location | How the agent uses it | Example | -|---|---|---| -| Header value | Agent reads `$API_KEY` from env and places it in a header. | `Authorization: Bearer ` | -| Header value (Basic auth) | Agent base64-encodes `user:` in an `Authorization: Basic` header. The proxy decodes, resolves, and re-encodes. | `Authorization: Basic ` | -| Query parameter value | Agent places the placeholder in a URL query parameter. | `GET /api?key=` | -| URL path segment | Agent builds a URL with the placeholder in the path. Supports concatenated patterns. | `POST /bot/sendMessage` | - -The proxy does not modify request bodies, cookies, or response content. - -### Fail-closed behavior - -If the proxy detects a credential placeholder in a request but cannot resolve it, it rejects the request with HTTP 500 instead of forwarding the raw placeholder to the upstream server. This prevents accidental credential leakage in server logs or error responses. - -### Example: Telegram Bot API (path-based credential) - -Create a provider with the Telegram bot token: - -```console -$ openshell provider create --name telegram --type generic --credential TELEGRAM_BOT_TOKEN=123456:ABC-DEF -``` - -The agent reads `TELEGRAM_BOT_TOKEN` from its environment and builds a request like `POST /bot/sendMessage`. The proxy resolves the placeholder in the URL path and forwards `POST /bot123456:ABC-DEF/sendMessage` to the upstream. - -### Example: Google API (query parameter credential) - -```console -$ openshell provider create --name google --type generic --credential YOUTUBE_API_KEY=AIzaSy-secret -``` - -The agent sends `GET /youtube/v3/search?part=snippet&key=`. The proxy resolves the placeholder in the query parameter value and percent-encodes the result before forwarding. - -## Supported Provider Types - -The following provider types are supported. - -| Type | Environment Variables Injected | Typical Use | -|---|---|---| -| `claude` | `ANTHROPIC_API_KEY`, `CLAUDE_API_KEY` | Claude Code, Anthropic API | -| `codex` | `OPENAI_API_KEY` | OpenAI Codex | -| `generic` | User-defined | Any service with custom credentials | -| `github` | `GITHUB_TOKEN`, `GH_TOKEN` | GitHub API, `gh` CLI — refer to {doc}`/tutorials/github-sandbox` | -| `gitlab` | `GITLAB_TOKEN`, `GLAB_TOKEN`, `CI_JOB_TOKEN` | GitLab API, `glab` CLI | -| `nvidia` | `NVIDIA_API_KEY` | NVIDIA API Catalog | -| `openai` | `OPENAI_API_KEY` | Any OpenAI-compatible endpoint. Set `--config OPENAI_BASE_URL` to point to the provider. Refer to {doc}`/inference/configure`. | -| `opencode` | `OPENCODE_API_KEY`, `OPENROUTER_API_KEY`, `OPENAI_API_KEY` | opencode tool | - -:::{tip} -Use the `generic` type for any service not listed above. You define the -environment variable names and values yourself with `--credential`. -::: - -## Supported Inference Providers - -The following providers have been tested with `inference.local`. Any provider that exposes an OpenAI-compatible API works with the `openai` type. Set `--config OPENAI_BASE_URL` to the provider's base URL and `--credential OPENAI_API_KEY` to your API key. - -| Provider | Name | Type | Base URL | API Key Variable | -|---|---|---|---|---| -| NVIDIA API Catalog | `nvidia-prod` | `nvidia` | `https://integrate.api.nvidia.com/v1` | `NVIDIA_API_KEY` | -| Anthropic | `anthropic-prod` | `anthropic` | `https://api.anthropic.com` | `ANTHROPIC_API_KEY` | -| Baseten | `baseten` | `openai` | `https://inference.baseten.co/v1` | `OPENAI_API_KEY` | -| Bitdeer AI | `bitdeer` | `openai` | `https://api-inference.bitdeer.ai/v1` | `OPENAI_API_KEY` | -| Deepinfra | `deepinfra` | `openai` | `https://api.deepinfra.com/v1/openai` | `OPENAI_API_KEY` | -| Groq | `groq` | `openai` | `https://api.groq.com/openai/v1` | `OPENAI_API_KEY` | -| Ollama (local) | `ollama` | `openai` | `http://host.openshell.internal:11434/v1` | `OPENAI_API_KEY` | -| LM Studio (local) | `lmstudio` | `openai` | `http://host.openshell.internal:1234/v1` | `OPENAI_API_KEY` | - -Refer to your provider's documentation for the correct base URL, available models, and API key setup. To configure inference routing, refer to {doc}`/inference/configure`. - -## Next Steps - -Explore related topics: - -- To control what the agent can access, refer to {doc}`policies`. -- To use a pre-built environment, refer to the {doc}`community-sandboxes` catalog. -- To view the complete field reference for the policy YAML, refer to the [Policy Schema Reference](../reference/policy-schema.md). \ No newline at end of file diff --git a/fern/pages/sandboxes/manage-providers.mdx b/docs/sandboxes/manage-providers.mdx similarity index 97% rename from fern/pages/sandboxes/manage-providers.mdx rename to docs/sandboxes/manage-providers.mdx index 9fee45794..fbfc4d380 100644 --- a/fern/pages/sandboxes/manage-providers.mdx +++ b/docs/sandboxes/manage-providers.mdx @@ -1,18 +1,12 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Providers" sidebar-title: "Providers" description: "Create and manage credential providers that inject API keys and tokens into OpenShell sandboxes." keywords: "Generative AI, Cybersecurity, Providers, Credentials, API Keys, Sandbox, Security" -tags: - - Providers - - Credentials - - API Keys - - Sandbox - - Security position: 4 --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} AI agents typically need credentials to access external services: an API key for the AI model provider, a token for GitHub or GitLab, and so on. OpenShell manages these credentials as first-class entities called *providers*. diff --git a/docs/sandboxes/manage-sandboxes.md b/docs/sandboxes/manage-sandboxes.md deleted file mode 100644 index 5306120a0..000000000 --- a/docs/sandboxes/manage-sandboxes.md +++ /dev/null @@ -1,192 +0,0 @@ ---- -title: - page: Manage Sandboxes - nav: Sandboxes -description: Set up gateways, create sandboxes, and manage the full sandbox lifecycle. -topics: -- Generative AI -- Cybersecurity -tags: -- Gateway -- Sandboxing -- AI Agents -- Sandbox Management -- CLI -content: - type: how_to - difficulty: technical_beginner - audience: - - engineer - - data_scientist ---- - - - -# Manage Sandboxes - -This page covers creating sandboxes and managing them. For background on what sandboxes are and how isolation works, refer to [About Sandboxes](index.md). - -:::{important} -Docker must be running before you create a gateway or sandbox. If it is not, the CLI -returns a connection-refused error (`os error 61`) without explaining -the cause. Start Docker and try again. -::: - -## Create a Sandbox - -Create a sandbox with a single command. For example, to create a sandbox with Claude, run: - -```console -$ openshell sandbox create -- claude -``` - -Every sandbox requires a gateway. If you run `openshell sandbox create` without a gateway, the CLI auto-bootstraps a local gateway. - -### Remote Gateways - -If you plan to run sandboxes on a remote host or a cloud-hosted gateway, set up the gateway first. Refer to {doc}`manage-gateways` for deployment options and multi-gateway management. - -### GPU Resources - -To request GPU resources, add `--gpu`: - -```console -$ openshell sandbox create --gpu -- claude -``` - -### Custom Containers - -Use `--from` to create a sandbox from a pre-built community package, a local directory, or a container image: - -```console -$ openshell sandbox create --from openclaw -$ openshell sandbox create --from ./my-sandbox-dir -$ openshell sandbox create --from my-registry.example.com/my-image:latest -``` - -The CLI resolves community names against the [OpenShell Community](https://github.com/NVIDIA/OpenShell-Community) catalog, pulls the bundled Dockerfile and policy, builds the image locally, and creates the sandbox. For the full catalog and how to contribute your own, refer to {doc}`community-sandboxes`. - -## Connect to a Sandbox - -Open an SSH session into a running sandbox: - -```console -$ openshell sandbox connect my-sandbox -``` - -Launch VS Code or Cursor directly into the sandbox workspace: - -```console -$ openshell sandbox create --editor vscode --name my-sandbox -$ openshell sandbox connect my-sandbox --editor cursor -``` - -When `--editor` is used, OpenShell keeps the sandbox alive and installs an -OpenShell-managed SSH include file instead of cluttering your main -`~/.ssh/config` with generated host blocks. - -## Monitor and Debug - -List all sandboxes: - -```console -$ openshell sandbox list -``` - -Get detailed information about a specific sandbox: - -```console -$ openshell sandbox get my-sandbox -``` - -Stream sandbox logs to monitor agent activity and diagnose policy decisions: - -```console -$ openshell logs my-sandbox -``` - -| Flag | Purpose | Example | -|---|---|---| -| `--tail` | Stream logs in real time | `openshell logs my-sandbox --tail` | -| `--source` | Filter by log source | `--source sandbox` | -| `--level` | Filter by severity | `--level warn` | -| `--since` | Show logs from a time window | `--since 5m` | - -OpenShell Terminal combines sandbox status and live logs in a single real-time dashboard: - -```console -$ openshell term -``` - -Use the terminal to spot blocked connections marked `action=deny` and inference-related proxy activity. If a connection is blocked unexpectedly, add the host to your network policy. Refer to {doc}`policies` for the workflow. - -## Port Forwarding - -Forward a local port to a running sandbox to access services inside it, such as a web server or database: - -```console -$ openshell forward start 8000 my-sandbox -$ openshell forward start 8000 my-sandbox -d # run in background -``` - -List and stop active forwards: - -```console -$ openshell forward list -$ openshell forward stop 8000 my-sandbox -``` - -:::{tip} -You can also forward a port at creation time with `--forward`: - -```console -$ openshell sandbox create --forward 8000 -- claude -``` -::: - -## SSH Config - -Generate an SSH config entry for a sandbox so tools like VS Code Remote-SSH can connect directly: - -```console -$ openshell sandbox ssh-config my-sandbox -``` - -Append the output to `~/.ssh/config` or use `--editor` on `sandbox create`/`sandbox connect` for automatic setup. - -## Transfer Files - -Upload files from your host into the sandbox: - -```console -$ openshell sandbox upload my-sandbox ./src /sandbox/src -``` - -Download files from the sandbox to your host: - -```console -$ openshell sandbox download my-sandbox /sandbox/output ./local -``` - -:::{note} -You can also upload files at creation time with the `--upload` flag on -`openshell sandbox create`. -::: - -## Delete Sandboxes - -Deleting a sandbox stops all processes, releases resources, and purges injected credentials. - -```console -$ openshell sandbox delete my-sandbox -``` - -## Next Steps - -- To follow a complete end-to-end example, refer to the {doc}`/tutorials/github-sandbox` tutorial. -- To supply API keys or tokens, refer to {doc}`manage-providers`. -- To control what the agent can access, refer to {doc}`policies`. -- To use a pre-built environment, refer to the {doc}`community-sandboxes` catalog. diff --git a/fern/pages/sandboxes/manage-sandboxes.mdx b/docs/sandboxes/manage-sandboxes.mdx similarity index 95% rename from fern/pages/sandboxes/manage-sandboxes.mdx rename to docs/sandboxes/manage-sandboxes.mdx index 42c1ff237..b43864401 100644 --- a/fern/pages/sandboxes/manage-sandboxes.mdx +++ b/docs/sandboxes/manage-sandboxes.mdx @@ -1,18 +1,12 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Manage Sandboxes" sidebar-title: "Sandboxes" description: "Set up gateways, create sandboxes, and manage the full sandbox lifecycle." keywords: "Generative AI, Cybersecurity, Gateway, Sandboxing, AI Agents, Sandbox Management, CLI" -tags: - - Gateway - - Sandboxing - - AI Agents - - Sandbox Management - - CLI position: 2 --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} This page covers creating sandboxes and managing them. For background on what sandboxes are and how isolation works, refer to [About Sandboxes](/sandboxes/about). diff --git a/docs/sandboxes/policies.md b/docs/sandboxes/policies.md deleted file mode 100644 index 3ec33af9e..000000000 --- a/docs/sandboxes/policies.md +++ /dev/null @@ -1,304 +0,0 @@ ---- -title: - page: Customize Sandbox Policies - nav: Policies -description: Apply, iterate, and debug sandbox network policies with hot-reload on running OpenShell sandboxes. -topics: -- Generative AI -- Cybersecurity -tags: -- Policy -- Network Policy -- Sandbox -- Security -- Hot Reload -content: - type: how_to - difficulty: technical_intermediate - audience: - - engineer ---- - - - -# Customize Sandbox Policies - -Use this page to apply and iterate policy changes on running sandboxes. For a full field-by-field YAML definition, use the [Policy Schema Reference](../reference/policy-schema.md). - -## Policy Structure - -A policy has static sections `filesystem_policy`, `landlock`, and `process` that are locked at sandbox creation, and a dynamic section `network_policies` that is hot-reloadable on a running sandbox. - -```yaml -version: 1 - -# Static: locked at sandbox creation. Paths the agent can read vs read/write. -filesystem_policy: - read_only: [/usr, /lib, /etc] - read_write: [/sandbox, /tmp] - -# Static: Landlock LSM kernel enforcement. best_effort uses highest ABI the host supports. -landlock: - compatibility: best_effort - -# Static: Unprivileged user/group the agent process runs as. -process: - run_as_user: sandbox - run_as_group: sandbox - -# Dynamic: hot-reloadable. Named blocks of endpoints + binaries allowed to reach them. -network_policies: - my_api: - name: my-api - endpoints: - - host: api.example.com - port: 443 - protocol: rest - enforcement: enforce - access: full - binaries: - - path: /usr/bin/curl - -``` - -Static sections are locked at sandbox creation. Changing them requires destroying and recreating the sandbox. -Dynamic sections can be updated on a running sandbox with `openshell policy set` and take effect without restarting. - -| Section | Type | Description | -|---|---|---| -| `filesystem_policy` | Static | Controls which directories the agent can access on disk. Paths are split into `read_only` and `read_write` lists. Any path not listed in either list is inaccessible. Set `include_workdir: true` to automatically add the agent's working directory to `read_write`. [Landlock LSM](https://docs.kernel.org/security/landlock.html) enforces these restrictions at the kernel level. | -| `landlock` | Static | Configures Landlock LSM enforcement behavior. Set `compatibility` to `best_effort` (skip individual inaccessible paths while applying remaining rules) or `hard_requirement` (fail if any path is inaccessible or the required kernel ABI is unavailable). See the [Policy Schema Reference](../reference/policy-schema.md#landlock) for the full behavior table. | -| `process` | Static | Sets the OS-level identity for the agent process. `run_as_user` and `run_as_group` default to `sandbox`. Root (`root` or `0`) is rejected. The agent also runs with seccomp filters that block dangerous system calls. | -| `network_policies` | Dynamic | Controls network access for ordinary outbound traffic from the sandbox. Each block has a name, a list of endpoints (host, port, protocol, and optional rules), and a list of binaries allowed to use those endpoints.
Every outbound connection except `https://inference.local` goes through the proxy, which queries the {doc}`policy engine <../about/architecture>` with the destination and calling binary. A connection is allowed only when both match an entry in the same policy block.
For endpoints with `protocol: rest`, the proxy auto-detects TLS and terminates it so each HTTP request is checked against that endpoint's `rules` (method and path).
Endpoints without `protocol` allow the TCP stream through without inspecting payloads.
If no endpoint matches, the connection is denied. Configure managed inference separately through {doc}`../inference/configure`. | - -## Baseline Filesystem Paths - -When a sandbox runs in proxy mode (the default), OpenShell automatically adds baseline filesystem paths required for the sandbox child process to function: `/usr`, `/lib`, `/etc`, `/var/log` (read-only) and `/sandbox`, `/tmp` (read-write). Paths like `/app` are included in the baseline set but are only added if they exist in the container image. - -This filtering prevents a missing baseline path from degrading Landlock enforcement. Without it, a single missing path could cause the entire Landlock ruleset to fail, leaving the sandbox with no filesystem restrictions at all. - -User-specified paths in your policy YAML are not pre-filtered. If you list a path that does not exist: - -- In `best_effort` mode, the path is skipped with a warning and remaining rules are still applied. -- In `hard_requirement` mode, sandbox startup fails immediately. - -This distinction means baseline system paths degrade gracefully while user-specified paths surface configuration errors. - -## Apply a Custom Policy - -Pass a policy YAML file when creating the sandbox: - -```console -$ openshell sandbox create --policy ./my-policy.yaml -- claude -``` - -`openshell sandbox create` keeps the sandbox running after the initial command exits, which is useful when you plan to iterate on the policy. Add `--no-keep` if you want the sandbox deleted automatically instead. - -To avoid passing `--policy` every time, set a default policy with an environment variable: - -```console -$ export OPENSHELL_SANDBOX_POLICY=./my-policy.yaml -$ openshell sandbox create -- claude -``` - -The CLI uses the policy from `OPENSHELL_SANDBOX_POLICY` whenever `--policy` is not explicitly provided. - -## Iterate on a Running Sandbox - -To change what the sandbox can access, pull the current policy, edit the YAML, and push the update. The workflow is iterative: create the sandbox, monitor logs for denied actions, pull the policy, modify it, push, and verify. - -```{mermaid} -flowchart TD - A["1. Create sandbox with initial policy"] --> B["2. Monitor logs for denied actions"] - B --> C["3. Pull current policy"] - C --> D["4. Modify the policy YAML"] - D --> E["5. Push updated policy"] - E --> F["6. Verify the new revision loaded"] - F --> B - - style A fill:#76b900,stroke:#000000,color:#000000 - style B fill:#76b900,stroke:#000000,color:#000000 - style C fill:#76b900,stroke:#000000,color:#000000 - style D fill:#ffffff,stroke:#000000,color:#000000 - style E fill:#76b900,stroke:#000000,color:#000000 - style F fill:#76b900,stroke:#000000,color:#000000 - - linkStyle default stroke:#76b900,stroke-width:2px -``` - -The following steps outline the hot-reload policy update workflow. - -1. Create the sandbox with your initial policy by following [Apply a Custom Policy](#apply-a-custom-policy) above (or set `OPENSHELL_SANDBOX_POLICY`). - -2. Monitor denials. Each log entry shows host, port, binary, and reason. Alternatively, use `openshell term` for a live dashboard. - - ```console - $ openshell logs --tail --source sandbox - ``` - -3. Pull the current policy. Strip the metadata header (Version, Hash, Status) before reusing the file. - - ```console - $ openshell policy get --full > current-policy.yaml - ``` - -4. Edit the YAML: add or adjust `network_policies` entries, binaries, `access`, or `rules`. - -5. Push the updated policy. Exit codes: 0 = loaded, 1 = validation failed, 124 = timeout. - - ```console - $ openshell policy set --policy current-policy.yaml --wait - ``` - -6. Verify the new revision. If status is `loaded`, repeat from step 2 as needed; if `failed`, fix the policy and repeat from step 4. - - ```console - $ openshell policy list - ``` - -## Global Policy Override - -Use a global policy when you want one policy payload to apply to every sandbox. - -```console -$ openshell policy set --global --policy ./global-policy.yaml -``` - -When a global policy is configured: - -- The global payload is applied in full for all sandboxes. -- Sandbox-level policy updates are rejected until the global policy is removed. - -To restore sandbox-level policy control, delete the global policy setting: - -```console -$ openshell policy delete --global -``` - -You can inspect a sandbox's effective settings and policy source with: - -```console -$ openshell settings get -``` - -## Debug Denied Requests - -Check `openshell logs --tail --source sandbox` for the denied host, path, and binary. - -When triaging denied requests, check: - -- Destination host and port to confirm which endpoint is missing. -- Calling binary path to confirm which `binaries` entry needs to be added or adjusted. -- HTTP method and path (for REST endpoints) to confirm which `rules` entry needs to be added or adjusted. - -Then push the updated policy as described above. - -## Examples - -Add these blocks to the `network_policies` section of your sandbox policy. Apply with `openshell policy set --policy --wait`. -Use **Simple endpoint** for host-level allowlists and **Granular rules** for method/path control. - -:::::{tab-set} - -::::{tab-item} Simple endpoint -Allow `pip install` and `uv pip install` to reach PyPI: - -```yaml - pypi: - name: pypi - endpoints: - - host: pypi.org - port: 443 - - host: files.pythonhosted.org - port: 443 - binaries: - - { path: /usr/bin/pip } - - { path: /usr/local/bin/uv } -``` - -Endpoints without `protocol` use TCP passthrough, where the proxy allows the stream without inspecting payloads. -:::: - -::::{tab-item} Granular rules -Allow Claude and the GitHub CLI to reach `api.github.com` with per-path rules: read-only (GET, HEAD, OPTIONS) and GraphQL (POST) for all paths; full write access for `alpha-repo`; and create/edit issues only for `bravo-repo`. Replace `` with your GitHub org or username. - -:::{tip} -For an end-to-end walkthrough that combines this policy with a GitHub credential provider and sandbox creation, refer to {doc}`/tutorials/github-sandbox`. -::: - -```yaml - github_repos: - name: github_repos - endpoints: - - host: api.github.com - port: 443 - protocol: rest - enforcement: enforce - rules: - - allow: - method: GET - path: "/**" - - allow: - method: HEAD - path: "/**" - - allow: - method: OPTIONS - path: "/**" - - allow: - method: POST - path: "/graphql" - - allow: - method: "*" - path: "/repos//alpha-repo/**" - - allow: - method: POST - path: "/repos//bravo-repo/issues" - - allow: - method: PATCH - path: "/repos//bravo-repo/issues/*" - binaries: - - { path: /usr/local/bin/claude } - - { path: /usr/bin/gh } -``` - -Endpoints with `protocol: rest` enable HTTP request inspection. The proxy auto-detects TLS on HTTPS endpoints, terminates it, and checks each HTTP request against the `rules` list. -:::: - -::::: - -### Query parameter matching - -REST rules can also constrain query parameter values: - -```yaml - download_api: - name: download_api - endpoints: - - host: api.example.com - port: 443 - protocol: rest - enforcement: enforce - rules: - - allow: - method: GET - path: "/api/v1/download" - query: - slug: "skill-*" - version: - any: ["1.*", "2.*"] - binaries: - - { path: /usr/bin/curl } -``` - -`query` matchers are case-sensitive and run on decoded values. If a request has duplicate keys (for example, `tag=a&tag=b`), every value for that key must match the configured glob(s). - -## Next Steps - -Explore related topics: - -- To learn about network access rules and sandbox isolation layers, refer to {doc}`index`. -- To view the full field-by-field YAML definition, refer to the [Policy Schema Reference](../reference/policy-schema.md). -- To review the default policy breakdown, refer to {doc}`../reference/default-policy`. diff --git a/fern/pages/sandboxes/policies.mdx b/docs/sandboxes/policies.mdx similarity index 98% rename from fern/pages/sandboxes/policies.mdx rename to docs/sandboxes/policies.mdx index 2cf585457..8d4831f1b 100644 --- a/fern/pages/sandboxes/policies.mdx +++ b/docs/sandboxes/policies.mdx @@ -1,18 +1,12 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Customize Sandbox Policies" sidebar-title: "Policies" description: "Apply, iterate, and debug sandbox network policies with hot-reload on running OpenShell sandboxes." keywords: "Generative AI, Cybersecurity, Policy, Network Policy, Sandbox, Security, Hot Reload" -tags: - - Policy - - Network Policy - - Sandbox - - Security - - Hot Reload position: 5 --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} Use this page to apply and iterate policy changes on running sandboxes. For a full field-by-field YAML definition, use the [Policy Schema Reference](/reference/policy-schema). diff --git a/docs/security/best-practices.md b/docs/security/best-practices.md deleted file mode 100644 index a54896f5a..000000000 --- a/docs/security/best-practices.md +++ /dev/null @@ -1,319 +0,0 @@ ---- -title: - page: "OpenShell Security Best Practices — Controls, Risks, and Configuration Guidance" - nav: "Security Best Practices" -description: "A guide to every configurable security control in OpenShell: defaults, what you can change, and the risks of each choice." -topics: -- Generative AI -- Cybersecurity -tags: -- Security -- Policy -- Sandbox -- Landlock -- Seccomp -content: - type: concept - difficulty: intermediate - audience: - - engineer - - security_engineer -status: published ---- - - - -# Security Best Practices - -OpenShell enforces sandbox security across four layers: network, filesystem, process, and inference. -This page documents every configurable control, its default, what it protects, and the risk of relaxing it. - -For the full policy YAML schema, refer to the {doc}`../reference/policy-schema`. -For the architecture of each enforcement layer, refer to {doc}`../about/architecture`. - -:::{seealso} -If you use [NemoClaw](https://github.com/NVIDIA/NemoClaw) to run OpenClaw assistants, its [Security Best Practices](https://docs.nvidia.com/nemoclaw/latest/security/best-practices.html) guide covers additional entrypoint-level controls, policy presets, provider trust tiers, and posture profiles specific to the NemoClaw blueprint. -::: - -## Enforcement Layers - -OpenShell applies security controls at two enforcement points. -OpenShell locks static controls at sandbox creation and requires destroying and recreating the sandbox to change them. -You can update dynamic controls on a running sandbox with `openshell policy set`. - -:::{list-table} -:header-rows: 1 -:widths: 20 30 20 30 - -* - Layer - - What it protects - - Enforcement point - - Changeable at runtime - -* - Network - - Unauthorized outbound connections and data exfiltration. - - CONNECT proxy + OPA policy engine - - Yes. Use `openshell policy set` or operator approval in the TUI. - -* - Filesystem - - System binary tampering, credential theft, config manipulation. - - Landlock LSM (kernel level) - - No. Requires sandbox re-creation. - -* - Process - - Privilege escalation, fork bombs, dangerous syscalls. - - Seccomp BPF + privilege drop (`setuid`/`setgid`) - - No. Requires sandbox re-creation. - -* - Inference - - Credential exposure, unauthorized model access. - - Proxy intercept of `inference.local` - - Yes. Use `openshell inference set`. - -::: - -## Network Controls - -The CONNECT proxy and OPA policy engine enforce all network controls at the gateway level. - -### Deny-by-Default Egress - -Every outbound connection from the sandbox goes through the CONNECT proxy. -The proxy evaluates each connection against the OPA policy engine. -If no `network_policies` entry matches the destination host, port, and calling binary, the proxy denies the connection. - -| Aspect | Detail | -|---|---| -| Default | All egress denied. Only endpoints listed in `network_policies` can receive traffic. | -| What you can change | Add entries to `network_policies` in the policy YAML. Apply statically at creation (`--policy`) or dynamically (`openshell policy set`). | -| Risk if relaxed | Each allowed endpoint is a potential data exfiltration path. The agent can send workspace content, credentials, or conversation history to any reachable host. | -| Recommendation | Add only endpoints the agent needs for its task. Start with a minimal policy and use denied-request logs (`openshell logs --source sandbox`) to identify missing endpoints. | - -### Network Namespace Isolation - -The sandbox runs in a dedicated Linux network namespace with a veth pair. -All traffic routes through the host-side veth IP (`10.200.0.1`) where the proxy listens. -Even if a process ignores proxy environment variables, it can only reach the proxy. - -| Aspect | Detail | -|---|---| -| Default | Always active. The sandbox cannot bypass the proxy at the network level. | -| What you can change | This is not a user-facing knob. OpenShell always enforces it in proxy mode. | -| Risk if bypassed | Without network namespace isolation, a process could connect directly to the internet, bypassing all policy enforcement. | -| Recommendation | No action needed. OpenShell enforces this automatically. | - -### Binary Identity Binding - -The proxy identifies which binary initiated each connection by reading `/proc//exe` (the kernel-trusted executable path). -It walks the process tree for ancestor binaries and parses `/proc//cmdline` for script interpreters. -The proxy SHA256-hashes each binary on first use (trust-on-first-use). If someone replaces a binary mid-session, the hash mismatch triggers an immediate deny. - -| Aspect | Detail | -|---|---| -| Default | Every `network_policies` entry requires a `binaries` list. Only listed binaries can reach the associated endpoints. Binary paths support glob patterns (`*` for one path component, `**` for recursive). | -| What you can change | Add binaries to an endpoint entry. Use glob patterns for directory-scoped access (for example, `/sandbox/.vscode-server/**`). | -| Risk if relaxed | Broad glob patterns (like `/**`) allow any binary to reach the endpoint, defeating the purpose of binary-scoped enforcement. | -| Recommendation | Scope binaries to the specific executables that need each endpoint. Use narrow globs when the exact path varies (for example, across Python virtual environments). | - -### L4-Only vs L7 Inspection - -The `protocol` field on an endpoint controls whether the proxy inspects individual HTTP requests inside the tunnel. - -| Aspect | Detail | -|---|---| -| Default | Endpoints without a `protocol` field use L4-only enforcement: the proxy checks host, port, and binary, then relays the TCP stream without inspecting payloads. | -| What you can change | Add `protocol: rest` to enable per-request HTTP inspection. Pair it with `rules` (fine-grained method and path control) or `access` presets (`full`, `read-only`, `read-write`). | -| Risk if relaxed | L4-only endpoints allow the agent to send any data through the tunnel after the initial connection is permitted. The proxy cannot see HTTP methods, paths, or bodies. Adding `access: full` with `protocol: rest` enables inspection but permits all methods and paths, providing observability without restriction. | -| Recommendation | Use `protocol: rest` with specific `rules` for APIs where you want method and path control. Use `access: read-only` for read-only endpoints. Omit `protocol` for non-HTTP protocols (WebSocket, gRPC streaming). | - -### Enforcement Mode (`audit` vs `enforce`) - -When `protocol: rest` is active, the `enforcement` field controls whether the proxy blocks or logs rule violations. - -| Aspect | Detail | -|---|---| -| Default | `audit`. The proxy logs violations but forwards traffic. | -| What you can change | Set `enforcement: enforce` to block requests that do not match any `rules` entry. Denied requests receive a `403 Forbidden` response with a JSON body describing the violation. | -| Risk if relaxed | `audit` mode provides visibility but does not prevent unauthorized actions. An agent can still perform write or delete operations on an API even if the rules would deny them. | -| Recommendation | Start with `audit` to understand traffic patterns and verify that rules are correct. Switch to `enforce` once you have validated that the rules match the intended access pattern. | - -### TLS Handling - -The proxy auto-detects TLS on every tunnel by peeking the first bytes. -When a TLS ClientHello is detected, the proxy terminates TLS transparently using a per-sandbox ephemeral CA. -This enables credential injection and L7 inspection without explicit configuration. - -| Aspect | Detail | -|---|---| -| Default | Auto-detect and terminate. OpenShell generates the sandbox CA at startup and injects it into the process trust stores (`NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE`). | -| What you can change | Set `tls: skip` on an endpoint to disable TLS detection and termination for that endpoint. Use this for client-certificate mTLS to upstream or non-standard binary protocols. | -| Risk if relaxed | `tls: skip` disables credential injection and L7 inspection for that endpoint. The proxy relays encrypted traffic without seeing the contents. | -| Recommendation | Use auto-detect (the default) for most endpoints. Use `tls: skip` only when the upstream requires the client's own TLS certificate (mTLS) or uses a non-HTTP protocol. | - -### SSRF Protection - -After OPA policy allows a connection, the proxy resolves DNS and rejects connections where the resolved IP is internal (loopback, link-local, or RFC 1918 private). - -| Aspect | Detail | -|---|---| -| Default | The proxy blocks all private IPs. Loopback (`127.0.0.0/8`) and link-local (`169.254.0.0/16`) remain blocked even with `allowed_ips`. | -| What you can change | Add `allowed_ips` (CIDR notation) to an endpoint to permit connections to specific private IP ranges. | -| Risk if relaxed | Without SSRF protection, a misconfigured policy could allow the agent to reach cloud metadata services (`169.254.169.254`), internal databases, or other infrastructure endpoints through DNS rebinding. | -| Recommendation | Use `allowed_ips` only for known internal services. Scope the CIDR as narrowly as possible (for example, `10.0.5.20/32` for a single host). Loopback and link-local are always blocked regardless of `allowed_ips`. | - -### Operator Approval - -When the agent requests an endpoint not in the policy, OpenShell blocks it and surfaces the request in the TUI for operator review. -The system merges approved endpoints into the sandbox's policy as a new durable revision. - -| Aspect | Detail | -|---|---| -| Default | Enabled. The proxy blocks unlisted endpoints and requires approval. | -| What you can change | Approved endpoints persist across sandbox restarts within the same sandbox instance. They reset when the sandbox is destroyed and recreated. | -| Risk if relaxed | Approving an endpoint permanently widens the running sandbox's policy. Review each request before approving. | -| Recommendation | Use operator approval for exploratory work. For recurring endpoints, add them to the policy YAML with appropriate binary and path restrictions. To reset all approved endpoints, destroy and recreate the sandbox. | - -## Filesystem Controls - -Landlock LSM restricts which paths the sandbox process can read or write at the kernel level. - -### Landlock LSM - -Landlock enforces filesystem access at the kernel level. -Paths listed in `read_only` receive read-only access. -Paths listed in `read_write` receive full access. -All other paths are inaccessible. - -| Aspect | Detail | -|---|---| -| Default | `compatibility: best_effort`. Uses the highest kernel ABI available. The system skips missing paths with a warning. If the kernel does not support Landlock, the sandbox continues without filesystem restrictions. | -| What you can change | Set `compatibility: hard_requirement` to abort sandbox startup if Landlock is unavailable or any configured path cannot be opened. | -| Risk if relaxed | On kernels without Landlock (pre-5.13), or when all paths fail to open, the sandbox runs without kernel-level filesystem restrictions. The agent can access any file the process user can access. | -| Recommendation | Use `best_effort` for development. Use `hard_requirement` in environments where any gap in filesystem isolation is unacceptable. Run on Ubuntu 22.04+ or any kernel 5.13+ for Landlock support. | - -### Read-Only vs Read-Write Paths - -The policy separates filesystem paths into read-only and read-write groups. - -| Aspect | Detail | -|---|---| -| Default | System paths (`/usr`, `/lib`, `/etc`, `/var/log`) are read-only. Working paths (`/sandbox`, `/tmp`) are read-write. `/app` is conditionally included if it exists. | -| What you can change | Add or remove paths in `filesystem_policy.read_only` and `filesystem_policy.read_write`. | -| Risk if relaxed | Making system paths writable lets the agent replace binaries, modify TLS trust stores, or change DNS resolution. Validation rejects broad read-write paths (like `/`). | -| Recommendation | Keep system paths read-only. If the agent needs additional writable space, add a specific subdirectory. | - -### Path Validation - -OpenShell validates policies before they take effect. - -| Constraint | Behavior | -|---|---| -| Paths must be absolute (start with `/`). | Rejected with `INVALID_ARGUMENT`. | -| Paths must not contain `..` traversal. | Rejected with `INVALID_ARGUMENT`. | -| Read-write paths must not be overly broad (for example, `/` alone). | Rejected with `INVALID_ARGUMENT`. | -| Each path must not exceed 4096 characters. | Rejected with `INVALID_ARGUMENT`. | -| Combined `read_only` + `read_write` paths must not exceed 256. | Rejected with `INVALID_ARGUMENT`. | - -## Process Controls - -The sandbox supervisor drops privileges, applies seccomp filters, and enforces process-level restrictions during startup. - -### Privilege Drop - -The sandbox process runs as a non-root user after explicit privilege dropping. - -| Aspect | Detail | -|---|---| -| Default | `run_as_user: sandbox`, `run_as_group: sandbox`. The supervisor calls `setuid()`/`setgid()` with post-condition verification: confirms the effective UID/GID match the target and that `setuid(0)` fails (root cannot be re-acquired). | -| What you can change | Set `run_as_user` and `run_as_group` in the `process` section. Validation rejects root (`root` or `0`). | -| Risk if relaxed | Running as a higher-privilege user increases the impact of container escape vulnerabilities. | -| Recommendation | Keep the `sandbox` user. Do not attempt to set root. | - -### Seccomp Filters - -A BPF seccomp filter restricts which socket domains the sandbox process can use. - -| Aspect | Detail | -|---|---| -| Default | The filter allows `AF_INET` and `AF_INET6` (for proxy communication) and blocks `AF_NETLINK`, `AF_PACKET`, `AF_BLUETOOTH`, and `AF_VSOCK` with `EPERM`. The sandbox sets `PR_SET_NO_NEW_PRIVS` before applying the filter. | -| What you can change | This is not a user-facing knob. OpenShell enforces it automatically. | -| Risk if relaxed | `AF_NETLINK` allows manipulation of routing tables and firewall rules. `AF_PACKET` enables raw packet capture. `AF_VSOCK` enables VM socket communication. | -| Recommendation | No action needed. OpenShell enforces this automatically. | - -### Enforcement Application Order - -The sandbox supervisor applies enforcement in a specific order during process startup. -This ordering is intentional: privilege dropping needs `/etc/group` and `/etc/passwd`, which Landlock subsequently restricts. - -1. Network namespace entry (`setns`). -2. Privilege drop (`initgroups` + `setgid` + `setuid`). -3. Landlock filesystem restrictions. -4. Seccomp socket domain filters. - -## Inference Controls - -OpenShell routes all inference traffic through the gateway to isolate provider credentials from the sandbox. - -### Routed Inference through `inference.local` - -The proxy intercepts HTTPS CONNECT requests to `inference.local` and routes matching inference API requests through the sandbox-local router. -The agent never receives the provider API key. - -| Aspect | Detail | -|---|---| -| Default | Always active. The proxy handles `inference.local` before OPA policy evaluation. The gateway injects credentials on the host side. | -| What you can change | Configure inference routes with `openshell inference set`. | -| Risk if bypassed | If an inference provider's host is added directly to `network_policies`, the agent could reach it with a stolen or hardcoded key, bypassing credential isolation. | -| Recommendation | Do not add inference provider hosts to `network_policies`. Use OpenShell inference routing instead. | - -## Gateway Security - -The gateway secures communication between the CLI, sandbox pods, and external clients with mutual TLS and token-based authentication. - -### mTLS - -Communication between the CLI, sandbox pods, and the gateway is secured by mutual TLS. -OpenShell generates a cluster CA at bootstrap and distributes it through Kubernetes secrets. - -| Aspect | Detail | -|---|---| -| Default | mTLS required. Both client and server present certificates that the cluster CA signed. | -| What you can change | Enable dual-auth mode (`allow_unauthenticated=true`) for Cloudflare Tunnel deployments, or disable TLS entirely for trusted reverse-proxy setups. | -| Risk if relaxed | Dual-auth mode accepts clients without certificates and defers authentication to the HTTP layer (Cloudflare JWT). Disabling TLS removes transport-level authentication entirely. | -| Recommendation | Use mTLS (the default) unless deploying behind Cloudflare or a trusted reverse proxy. | - -### SSH Tunnel Authentication - -SSH connections to sandboxes pass through the gateway's HTTP CONNECT tunnel with token-based authentication and HMAC-SHA256 handshake verification (NSSH1 protocol). - -| Aspect | Detail | -|---|---| -| Default | Session tokens expire after 24 hours. Concurrent connections are limited to 10 per token and 20 per sandbox. | -| What you can change | Configure `ssh_session_ttl_secs`. Set to 0 for no expiry. | -| Risk if relaxed | Longer TTLs or no expiry increase the window for stolen token reuse. Higher connection limits increase the blast radius of a compromised token. | -| Recommendation | Keep the 24-hour default. Monitor connection counts through the TUI. | - -## Common Mistakes - -The following patterns weaken security without providing meaningful benefit. - -| Mistake | Why it matters | What to do instead | -|---------|---------------|-------------------| -| Omitting `protocol: rest` on REST API endpoints | Without `protocol: rest`, the proxy uses L4-only enforcement. It allows the TCP stream through after checking host, port, and binary, but cannot inspect individual HTTP requests. | Add `protocol: rest` with specific `rules` to enable per-request method and path control. | -| Using `access: full` when finer rules would suffice | `access: full` with `protocol: rest` enables inspection but allows all HTTP methods and paths. | Use `access: read-only` or explicit `rules` to restrict what the agent can do at the HTTP level. | -| Adding endpoints permanently when operator approval would suffice | Adding endpoints to the policy YAML makes them permanently reachable across all instances. | Use operator approval. Approved endpoints persist within the sandbox instance but reset on re-creation. | -| Using broad binary globs | A glob like `/**` allows any binary to reach the endpoint, defeating binary-scoped enforcement. | Scope globs to specific directories (for example, `/sandbox/.vscode-server/**`). | -| Skipping TLS termination on HTTPS APIs | Setting `tls: skip` disables credential injection and L7 inspection. | Use the default auto-detect behavior unless the upstream requires client-certificate mTLS. | -| Setting `enforcement: enforce` before auditing | Jumping to `enforce` without first running in `audit` mode risks breaking the agent's workflow. | Start with `audit`, review the logs, and switch to `enforce` once you have validated the rules. | - -## Related Topics - -- {doc}`../sandboxes/policies` for applying and iterating on sandbox policies. -- {doc}`../reference/policy-schema` for the full field-by-field YAML reference. -- {doc}`../reference/default-policy` for the built-in default policy breakdown. -- {doc}`../reference/gateway-auth` for gateway authentication details. -- {doc}`../about/architecture` for the system architecture. -- NemoClaw [Security Best Practices](https://docs.nvidia.com/nemoclaw/latest/security/best-practices.html) for entrypoint-level controls (capability drops, PATH hardening, build toolchain removal), policy presets, provider trust tiers, and posture profiles. diff --git a/fern/pages/security/best-practices.mdx b/docs/security/best-practices.mdx similarity index 98% rename from fern/pages/security/best-practices.mdx rename to docs/security/best-practices.mdx index 36aef6e8c..77f138c18 100644 --- a/fern/pages/security/best-practices.mdx +++ b/docs/security/best-practices.mdx @@ -1,19 +1,13 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "OpenShell Security Best Practices — Controls, Risks, and Configuration Guidance" sidebar-title: "Security Best Practices" slug: "security/best-practices" description: "A guide to every configurable security control in OpenShell: defaults, what you can change, and the risks of each choice." keywords: "Generative AI, Cybersecurity, Security, Policy, Sandbox, Landlock, Seccomp" -tags: - - Security - - Policy - - Sandbox - - Landlock - - Seccomp position: 1 --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} OpenShell enforces sandbox security across four layers: network, filesystem, process, and inference. This page documents every configurable control, its default, what it protects, and the risk of relaxing it. diff --git a/docs/tutorials/first-network-policy.md b/docs/tutorials/first-network-policy.md deleted file mode 100644 index 5011ac89c..000000000 --- a/docs/tutorials/first-network-policy.md +++ /dev/null @@ -1,232 +0,0 @@ ---- -title: - page: Write Your First Sandbox Network Policy - nav: First Network Policy -description: See how OpenShell network policies work by creating a sandbox, observing default-deny in action, and applying a fine-grained L7 read-only rule. -topics: -- Generative AI -- Cybersecurity -tags: -- Tutorial -- Policy -- Network Policy -- Sandbox -- Security -content: - type: tutorial - difficulty: technical_beginner - audience: - - engineer ---- - - - -# Write Your First Sandbox Network Policy - -This tutorial shows how OpenShell's network policy system works in under five minutes. You create a sandbox, watch a request get blocked by the default-deny policy, apply a fine-grained L7 rule, and verify that reads are allowed while writes are blocked, all without restarting anything. - -After completing this tutorial, you understand: - -- How default-deny networking blocks all outbound traffic from a sandbox. -- How to apply a network policy that grants read-only access to a specific API. -- How L7 enforcement distinguishes between HTTP methods such as GET and POST on the same endpoint. -- How to inspect deny logs for a complete audit trail. - -## Prerequisites - -- A working OpenShell installation. Complete the {doc}`/get-started/quickstart` before proceeding. -- Docker Desktop running on your machine. - -:::{tip} -To run every step of this tutorial, you can also use the automated demo script at the [examples/sandbox-policy-quickstart](https://github.com/NVIDIA/OpenShell/blob/main/examples/sandbox-policy-quickstart) directory in the NVIDIA OpenShell repository. It runs the full walkthrough in under a minute but without any user interaction. - -```console -$ bash examples/sandbox-policy-quickstart/demo.sh -``` - -::: - -## Create a Sandbox - -Start by creating a sandbox with no network policies. This gives you a clean environment to observe default-deny behavior. - -```console -$ openshell sandbox create --name demo --keep --no-auto-providers -``` - -`--keep` keeps the sandbox running after you exit so you can reconnect later. `--no-auto-providers` skips the provider setup prompt since this tutorial uses `curl` instead of an AI agent. - -You land in an interactive shell inside the sandbox: - -```text -sandbox@demo:~$ -``` - -## Try to Reach the GitHub API - -With no network policy in place, every outbound connection is blocked. Test this by making a simple API call from inside the sandbox: - -```console -$ curl -s https://api.github.com/zen -``` - -`https://api.github.com/zen` is a lightweight, unauthenticated GitHub REST endpoint that returns a random aphorism on each call. It requires no tokens or parameters, which makes it a convenient smoke-test target for verifying outbound HTTPS connectivity. - -The request fails. By default, all outbound network traffic is denied. The sandbox proxy intercepted the HTTPS CONNECT request to `api.github.com:443` and rejected it because no network policy authorizes `curl` to reach that host. - -```text -curl: (56) Received HTTP code 403 from proxy after CONNECT -``` - -Exit the sandbox. The `--keep` flag keeps it running: - -```console -$ exit -``` - -## Check the Deny Log - -Every denied connection produces a structured log entry. Query the sandbox logs from your host to confirm the denial and inspect the reason. - -```console -$ openshell logs demo --since 5m -``` - -You see a line like: - -```text -action=deny dst_host=api.github.com dst_port=443 binary=/usr/bin/curl deny_reason="no matching network policy" -``` - -Every denied connection is logged with the destination, the binary that attempted it, and the reason. Nothing gets out silently. - -## Apply a Read-Only GitHub API Policy - -To allow the sandbox to reach the GitHub API, define a network policy that grants read-only access. The policy specifies which host, port, binary, and HTTP methods are permitted. Create a file called `github-readonly.yaml` with the following content: - -```yaml -version: 1 - -filesystem_policy: - include_workdir: true - read_only: [/usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log] - read_write: [/sandbox, /tmp, /dev/null] -landlock: - compatibility: best_effort -process: - run_as_user: sandbox - run_as_group: sandbox - -network_policies: - github_api: - name: github-api-readonly - endpoints: - - host: api.github.com - port: 443 - protocol: rest - enforcement: enforce - access: read-only - binaries: - - { path: /usr/bin/curl } -``` - -The `filesystem_policy`, `landlock`, and `process` sections preserve the default sandbox settings. This is required because `policy set` replaces the entire policy. The `network_policies` section is the key part: `curl` may make GET, HEAD, and OPTIONS requests to `api.github.com` over HTTPS. Everything else is denied. The proxy auto-detects TLS on HTTPS endpoints and terminates it to inspect each HTTP request and enforce the `read-only` access preset at the method level. - -Apply it: - -```console -$ openshell policy set demo --policy github-readonly.yaml --wait -``` - -`--wait` blocks until the sandbox confirms the new policy is loaded. No restart required. Policies are hot-reloaded. - -:::{tip} -This tutorial uses `curl` and `read-only` access to keep things simple. When building policies for real workloads: - -- To scope the policy to an agent, replace the `binaries` section with your agent's binary, such as `/usr/local/bin/claude`, instead of `curl`. -- To grant write access, change `access: read-only` to `read-write` or add explicit `rules` for specific paths. Refer to the {doc}`/reference/policy-schema`. -- To allow additional endpoints, stack multiple policies in the same file for PyPI, npm, or your internal APIs. Refer to {doc}`/sandboxes/policies` for examples. -::: - -## Verify If GET Requests Are Allowed - -The policy is now active. Reconnect to the sandbox and retry the same request to confirm that read access works. - -```console -$ openshell sandbox connect demo -``` - -Retry the same request: - -```console -$ curl -s https://api.github.com/zen -``` - -```text -Anything added dilutes everything else. -``` - -It works. The `read-only` preset allows GET requests through. - -## Try a Write - -The read-only preset allows GET but blocks mutating methods like POST, PUT, and DELETE. Test this by sending a POST request to the GitHub API while still inside the sandbox: - -```console -$ curl -s -X POST https://api.github.com/repos/octocat/hello-world/issues \ - -H "Content-Type: application/json" \ - -d '{"title":"oops"}' -``` - -```json -{"error":"policy_denied","policy":"github-api-readonly","detail":"POST /repos/octocat/hello-world/issues not permitted by policy"} -``` - -The CONNECT request succeeded because `api.github.com` is allowed, but the L7 proxy inspected the HTTP method and returned `403`. `POST` is not in the `read-only` preset. An agent with this policy can read code from GitHub but cannot create issues, push commits, or modify anything. - -Exit the sandbox: - -```console -$ exit -``` - -## Check the L7 Deny Log - -L7 denials are logged separately from connection-level denials. The log entry includes the exact HTTP method and path that the proxy rejected. - -```console -$ openshell logs demo --level warn --since 5m -``` - -```text -l7_decision=deny dst_host=api.github.com l7_action=POST l7_target=/repos/octocat/hello-world/issues l7_deny_reason="POST /repos/octocat/hello-world/issues not permitted by policy" -``` - -The log captures the exact HTTP method, path, and deny reason. In production, pipe these logs to your SIEM for a complete audit trail of every request your agent makes. - -:::{tip} -To log violations without blocking requests, set `enforcement: audit` instead of `enforcement: enforce` in the policy. This is useful for building a policy iteratively: deploy in audit mode, review the logs, and switch to enforce when the rules are correct. -::: - -## Clean Up - -Delete the sandbox to free resources. This stops all processes and purges any injected credentials. - -```console -$ openshell sandbox delete demo -``` - -:::{tip} -To run this entire walkthrough non-interactively, use the automated demo script: - -```console -$ bash examples/sandbox-policy-quickstart/demo.sh -``` -::: - -## Next Steps - -- To walk through a full policy iteration with Claude Code, including diagnosing denials and applying fixes from outside the sandbox, refer to {doc}`/tutorials/github-sandbox`. diff --git a/fern/pages/tutorials/first-network-policy.mdx b/docs/tutorials/first-network-policy.mdx similarity index 97% rename from fern/pages/tutorials/first-network-policy.mdx rename to docs/tutorials/first-network-policy.mdx index 7f8357a29..bb68e6135 100644 --- a/fern/pages/tutorials/first-network-policy.mdx +++ b/docs/tutorials/first-network-policy.mdx @@ -1,18 +1,12 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Write Your First Sandbox Network Policy" sidebar-title: "First Network Policy" slug: "tutorials/first-network-policy" description: "See how OpenShell network policies work by creating a sandbox, observing default-deny in action, and applying a fine-grained L7 read-only rule." keywords: "Generative AI, Cybersecurity, Tutorial, Policy, Network Policy, Sandbox, Security" -tags: - - Tutorial - - Policy - - Network Policy - - Sandbox - - Security --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} This tutorial shows how OpenShell's network policy system works in under five minutes. You create a sandbox, watch a request get blocked by the default-deny policy, apply a fine-grained L7 rule, and verify that reads are allowed while writes are blocked, all without restarting anything. diff --git a/docs/tutorials/github-sandbox.md b/docs/tutorials/github-sandbox.md deleted file mode 100644 index 9372ca294..000000000 --- a/docs/tutorials/github-sandbox.md +++ /dev/null @@ -1,380 +0,0 @@ ---- -title: - page: Grant GitHub Push Access to a Sandboxed Agent - nav: GitHub Push Access -description: Learn the iterative policy workflow by launching a sandbox, diagnosing a GitHub access denial, and applying a custom policy to fix it. -topics: -- Generative AI -- Cybersecurity -tags: -- Tutorial -- GitHub -- Sandbox -- Policy -- Claude Code -content: - type: tutorial - difficulty: technical_intermediate - audience: - - engineer ---- - - - -# Grant GitHub Push Access to a Sandboxed Agent - -This tutorial walks through an iterative sandbox policy workflow. You launch a sandbox, ask Claude Code to push code to GitHub, and observe the default network policy denying the request. -You then diagnose the denial from your machine and from inside the sandbox, apply a policy update, and verify that the policy update to the sandbox takes effect. - -After completing this tutorial, you will have: - -- A running sandbox with Claude Code that can push to a GitHub repository. -- A custom network policy that grants GitHub access for a specific repository. -- Experience with the policy iteration workflow: fail, diagnose, update, verify. - -:::{note} -This tutorial shows example prompts and responses from Claude Code. The exact wording you see might vary between sessions. Use the examples as a guide for the type of interaction, not as expected output. -::: - -## Prerequisites - -This tutorial requires the following: - -- A working OpenShell installation. Complete the {doc}`Quickstart ` before proceeding. -- A GitHub personal access token (PAT) with `repo` scope. Generate one from the [GitHub personal access token settings page](https://github.com/settings/tokens) by selecting **Generate new token (classic)** and enabling the `repo` scope. -- An [Anthropic account](https://console.anthropic.com/) with access to Claude Code. OpenShell provides the sandbox runtime, not the agent. You must authenticate with your own account. -- A GitHub repository you own to use as the push target. A scratch repository is sufficient. You can [create one](https://github.com/new) with a README if needed. - -This tutorial uses two terminals to demonstrate the iterative policy workflow: - -- **Terminal 1**: The sandbox terminal. You create the sandbox in this terminal by running `openshell sandbox create` and interact with Claude Code inside it. -- **Terminal 2**: A terminal outside the sandbox on your machine. You use this terminal for viewing the sandbox logs with `openshell term` and applying an updated policy with `openshell policy set`. - -Each section below indicates which terminal to use. - -## Set Up a Sandbox with Your GitHub Token - -Depending on whether you start a new sandbox or use an existing sandbox, choose the appropriate tab and follow the instructions. - -::::{tab-set} - -:::{tab-item} Starting a new sandbox - -In terminal 2, create a new sandbox with Claude Code. The {doc}`default policy ` is applied automatically, which allows read-only access to GitHub. - -Create a {doc}`credential provider ` that injects your GitHub token into the sandbox automatically. The provider reads `GITHUB_TOKEN` from your host environment and sets it as an environment variable inside the sandbox: - -```console -$ GITHUB_TOKEN= -$ openshell provider create --name my-github --type github --from-existing -$ openshell sandbox create --provider my-github -- claude -``` - -`openshell sandbox create` keeps the sandbox running after Claude Code exits, so you can apply policy updates later without recreating the environment. Add `--no-keep` if you want the sandbox deleted automatically instead. - -Claude Code starts inside the sandbox. It prints an authentication link. Open it in your browser, sign in to your Anthropic account, and return to the terminal. When prompted, trust the `/sandbox` workspace to allow Claude Code to read and write files. -::: - -:::{tab-item} Using an existing sandbox - -In terminal 1, connect to a sandbox that is already running and set your GitHub token as an environment variable: - -```console -$ openshell sandbox connect -$ export GITHUB_TOKEN= -``` - -To find the name of running sandboxes, run `openshell sandbox list` in terminal 2. - -::: - -:::: - -## Push Code to GitHub - -In terminal 1, ask Claude Code to write a simple script and push it to your repository. Replace `` with your GitHub organization or username and `` with your repository name. - -:::{dropdown} Prompt -:open: -:icon: terminal - -Write a `hello_world.py` script and push it to `https://github.com//`. -::: - -Claude recognizes that it needs GitHub credentials. It asks how you want to authenticate. Provide your GitHub personal access token by pasting it into the conversation. Claude configures authentication and attempts the push. - -The push fails. Claude reports an error, but the failure is not an authentication problem. The default sandbox policy permits read-only access to GitHub and blocks write operations, so the proxy denies the push before the request reaches the GitHub server. - -## Diagnose the Denial - -In this section, you diagnose the denial from your machine and from inside the sandbox. - -### View the Logs from Your Machine - -In terminal 2, launch the OpenShell terminal: - -```console -$ openshell term -``` - -The dashboard shows sandbox status and a live stream of policy decisions. Look for entries with `l7_decision=deny`. Select a deny entry to see the full detail: - -```text -l7_action: PUT -l7_target: /repos///contents/hello_world.py -l7_decision: deny -dst_host: api.github.com -dst_port: 443 -l7_protocol: rest -policy: github_rest_api -l7_deny_reason: PUT /repos///contents/hello_world.py not permitted by policy -``` - -The log shows that the sandbox proxy intercepted an outbound `PUT` request to `api.github.com` and denied it. The `github_rest_api` policy allows read operations (GET) but blocks write operations (PUT, POST, DELETE) to the GitHub API. A similar denial appears for `github.com` if Claude attempted a git push over HTTPS. - -### Ask Claude Code to Check the Sandbox Logs - -In terminal 1, ask Claude Code to check the sandbox logs for denied requests: - -:::{dropdown} Prompt -:open: -:icon: terminal - -Check the sandbox logs for any denied network requests. What is blocking the push? -::: - -Claude reads the deny entries and identifies the root cause. It explains that the failure is a sandbox network policy restriction, not a token permissions issue. For example, the following is a possible response: - -:::{dropdown} Response -:open: -:icon: comment - -The sandbox runs a proxy that enforces policies on outbound traffic. -The `github_rest_api` policy allows GET requests (used to read the file) -but blocks PUT/write requests to GitHub. This is a sandbox-level restriction, -not a token issue. No matter what token you provide, pushes through the API -will be blocked until the policy is updated. -::: - -Both perspectives confirm the same thing: the proxy is doing its job. The default policy is designed to be restrictive. To allow GitHub pushes, you need to update the network policy. - -Copy the deny reason from Claude's response. You paste it into an agent running on your machine in the next step. - -## Update the Policy from Your Machine - -In terminal 2, paste the deny reason from the previous step into your coding agent on your machine, such as Claude Code or Cursor, and ask it to recommend a policy update. The deny reason gives the agent the context it needs to generate the correct policy rules. After pasting the following prompt sample, properly provide the GitHub organization and repository names of the repository you are pushing to. - -:::{dropdown} Prompt -:open: -:icon: terminal - -Based on the following deny reasons, recommend a sandbox policy update that allows GitHub pushes to `https://github.com//`, and save to `/tmp/sandbox-policy-update.yaml`: - -The `filesystem_policy`, `landlock`, and `process` sections are static. They are read once at sandbox creation and cannot be changed by a hot-reload. They are included here for completeness so the file is self-contained, but only the `network_policies` section takes effect when you apply this to a running sandbox. -::: - -The following steps outline the expected process done by the agent: - -1. Inspects the deny reasons. -2. Writes an updated policy that adds `github_git` and `github_api` blocks that grant write access to your repository. -3. Saves the policy to `/tmp/sandbox-policy-update.yaml`. - -## Review the Generated Policy - -Refer to the following policy example to compare with the generated policy before applying it. Confirm that the policy grants only the access you expect. In this case, `git push` operations and GitHub REST API access scoped to a single repository. - -::::{dropdown} Full reference policy -:icon: code - -The following YAML shows a complete policy that extends the {doc}`default policy ` with GitHub access for a single repository. Replace `` with your GitHub organization or username and `` with your repository name. - -The `filesystem_policy`, `landlock`, and `process` sections are static. They are read once at sandbox creation and cannot be changed by a hot-reload. They are included here for completeness so the file is self-contained, but only the `network_policies` section takes effect when you apply this to a running sandbox. - -```{code-block} yaml -:emphasize-lines: 54-100 - -version: 1 - -# ── Static (locked at sandbox creation) ────────────────────────── - -filesystem_policy: - include_workdir: true - read_only: - - /usr - - /lib - - /proc - - /dev/urandom - - /app - - /etc - - /var/log - read_write: - - /sandbox - - /tmp - - /dev/null - -landlock: - compatibility: best_effort - -process: - run_as_user: sandbox - run_as_group: sandbox - -# ── Dynamic (hot-reloadable) ───────────────────────────────────── - -network_policies: - - # Claude Code ↔ Anthropic API - claude_code: - name: claude-code - endpoints: - - { host: api.anthropic.com, port: 443, protocol: rest, enforcement: enforce, access: full } - - { host: statsig.anthropic.com, port: 443 } - - { host: sentry.io, port: 443 } - - { host: raw.githubusercontent.com, port: 443 } - - { host: platform.claude.com, port: 443 } - binaries: - - { path: /usr/local/bin/claude } - - { path: /usr/bin/node } - - # NVIDIA inference endpoint - nvidia_inference: - name: nvidia-inference - endpoints: - - { host: integrate.api.nvidia.com, port: 443 } - binaries: - - { path: /usr/bin/curl } - - { path: /bin/bash } - - { path: /usr/local/bin/opencode } - - # ── GitHub: git operations (clone, fetch, push) ────────────── - - github_git: - name: github-git - endpoints: - - host: github.com - port: 443 - protocol: rest - enforcement: enforce - rules: - - allow: - method: GET - path: "//.git/info/refs*" - - allow: - method: POST - path: "//.git/git-upload-pack" - - allow: - method: POST - path: "//.git/git-receive-pack" - binaries: - - { path: /usr/bin/git } - - # ── GitHub: REST API ───────────────────────────────────────── - - github_api: - name: github-api - endpoints: - - host: api.github.com - port: 443 - protocol: rest - enforcement: enforce - rules: - # GraphQL API (used by gh CLI) - - allow: - method: POST - path: "/graphql" - # Full read-write access to the repository - - allow: - method: "*" - path: "/repos///**" - binaries: - - { path: /usr/local/bin/claude } - - { path: /usr/local/bin/opencode } - - { path: /usr/bin/gh } - - { path: /usr/bin/curl } - - # ── Package managers ───────────────────────────────────────── - - pypi: - name: pypi - endpoints: - - { host: pypi.org, port: 443 } - - { host: files.pythonhosted.org, port: 443 } - - { host: github.com, port: 443 } - - { host: objects.githubusercontent.com, port: 443 } - - { host: api.github.com, port: 443 } - - { host: downloads.python.org, port: 443 } - binaries: - - { path: /sandbox/.venv/bin/python } - - { path: /sandbox/.venv/bin/python3 } - - { path: /sandbox/.venv/bin/pip } - - { path: "/sandbox/.uv/python/**/python*" } - - { path: /usr/local/bin/uv } - - { path: "/sandbox/.uv/python/**" } - - # ── VS Code Remote ────────────────────────────────────────── - - vscode: - name: vscode - endpoints: - - { host: update.code.visualstudio.com, port: 443 } - - { host: "*.vo.msecnd.net", port: 443 } - - { host: vscode.download.prss.microsoft.com, port: 443 } - - { host: marketplace.visualstudio.com, port: 443 } - - { host: "*.gallerycdn.vsassets.io", port: 443 } - binaries: - - { path: /usr/bin/curl } - - { path: /usr/bin/wget } - - { path: "/sandbox/.vscode-server/**" } - - { path: "/sandbox/.vscode-remote-containers/**" } -``` - -The following table summarizes the two GitHub-specific blocks: - -| Block | Endpoint | Behavior | -|---|---|---| -| `github_git` | `github.com:443` | Git Smart HTTP protocol. The proxy auto-detects and terminates TLS to inspect requests. Permits `info/refs` (clone/fetch), `git-upload-pack` (fetch data), and `git-receive-pack` (push) for the specified repository. Denies all operations on unlisted repositories. | -| `github_api` | `api.github.com:443` | REST API. The proxy auto-detects and terminates TLS to inspect requests. Permits all HTTP methods for the specified repository and GraphQL queries. Denies API access to unlisted repositories. | - -The remaining blocks (`claude_code`, `nvidia_inference`, `pypi`, `vscode`) are identical to the {doc}`default policy `. The default policy's `github_ssh_over_https` and `github_rest_api` blocks are replaced by the `github_git` and `github_api` blocks above, which grant write access to the specified repository. Sandbox behavior outside of GitHub operations is unchanged. - -For details on policy block structure, refer to [Policies](/sandboxes/policies.md). -:::: - -## Apply the Policy - -After you have reviewed the generated policy, apply it to the running sandbox: - -```console -$ openshell policy set --policy /tmp/sandbox-policy-update.yaml --wait -``` - -Network policies are hot-reloadable. The `--wait` flag blocks until the policy engine confirms the new revision loaded, and the update takes effect immediately without restarting the sandbox or reconnecting Claude Code. - -## Retry the Push - -In terminal 1, ask Claude Code to retry the push: - -```text -The sandbox policy has been updated. Try pushing to the repository again. -``` - -The push completes successfully. The `openshell term` dashboard now shows `l7_decision=allow` entries for `api.github.com` and `github.com` where it previously showed denials. - -## Clean Up - -When you are finished, delete the sandbox to free cluster resources: - -```console -$ openshell sandbox delete -``` - -## Next Steps - -The following resources cover related topics in greater depth: - -- To add per-repository access levels (read-write vs read-only) or restrict to specific API methods, refer to the [Policy Schema Reference](/reference/policy-schema.md). -- To learn the full policy iteration workflow (pull, edit, push, verify), refer to {doc}`/sandboxes/policies`. -- To inject credentials automatically instead of pasting tokens, refer to {doc}`/sandboxes/manage-providers`. diff --git a/fern/pages/tutorials/github-sandbox.mdx b/docs/tutorials/github-sandbox.mdx similarity index 98% rename from fern/pages/tutorials/github-sandbox.mdx rename to docs/tutorials/github-sandbox.mdx index c08bbdfab..0490b6bc1 100644 --- a/fern/pages/tutorials/github-sandbox.mdx +++ b/docs/tutorials/github-sandbox.mdx @@ -1,18 +1,12 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Grant GitHub Push Access to a Sandboxed Agent" sidebar-title: "GitHub Push Access" slug: "tutorials/github-sandbox" description: "Learn the iterative policy workflow by launching a sandbox, diagnosing a GitHub access denial, and applying a custom policy to fix it." keywords: "Generative AI, Cybersecurity, Tutorial, GitHub, Sandbox, Policy, Claude Code" -tags: - - Tutorial - - GitHub - - Sandbox - - Policy - - Claude Code --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} This tutorial walks through an iterative sandbox policy workflow. You launch a sandbox, ask Claude Code to push code to GitHub, and observe the default network policy denying the request. You then diagnose the denial from your machine and from inside the sandbox, apply a policy update, and verify that the policy update to the sandbox takes effect. diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md deleted file mode 100644 index e3f029c24..000000000 --- a/docs/tutorials/index.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -title: - page: Tutorials - nav: Tutorials -description: Step-by-step walkthroughs for OpenShell, from first sandbox to production-ready policies. -topics: -- Generative AI -- Cybersecurity -tags: -- Tutorial -- Sandbox -- Policy -content: - type: index ---- - - - -# Tutorials - -Hands-on walkthroughs that teach OpenShell concepts by building real configurations. Each tutorial builds on the previous one, starting with core sandbox mechanics and progressing to production workflows. - -::::{grid} 1 1 2 2 -:gutter: 3 - -:::{grid-item-card} First Network Policy -:link: first-network-policy -:link-type: doc - -Create a sandbox, observe default-deny networking, apply a read-only L7 policy, and inspect audit logs. No AI agent required. -+++ -{bdg-secondary}`Tutorial` -::: - -:::{grid-item-card} GitHub Push Access -:link: github-sandbox -:link-type: doc - -Launch Claude Code in a sandbox, diagnose a policy denial, and iterate on a custom GitHub policy from outside the sandbox. -+++ -{bdg-secondary}`Tutorial` -::: - -:::{grid-item-card} Inference with Ollama -:link: inference-ollama -:link-type: doc - -Route inference through Ollama using cloud-hosted or local models, and verify it from a sandbox. -+++ -{bdg-secondary}`Tutorial` -::: - -:::{grid-item-card} Local Inference with LM Studio -:link: local-inference-lmstudio -:link-type: doc - -Route inference to a local LM Studio server via the OpenAI or Anthropic compatible APIs. -+++ -{bdg-secondary}`Tutorial` -::: -:::: - -```{toctree} -:hidden: - -First Network Policy -GitHub Push Access -Inference with Ollama -Local Inference with LM Studio -``` diff --git a/fern/pages/tutorials/index.mdx b/docs/tutorials/index.mdx similarity index 86% rename from fern/pages/tutorials/index.mdx rename to docs/tutorials/index.mdx index e523f46f6..970633a88 100644 --- a/fern/pages/tutorials/index.mdx +++ b/docs/tutorials/index.mdx @@ -1,16 +1,12 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Tutorials" slug: "tutorials" description: "Step-by-step walkthroughs for OpenShell, from first sandbox to production-ready policies." keywords: "Generative AI, Cybersecurity, Tutorial, Sandbox, Policy" -tags: - - Tutorial - - Sandbox - - Policy position: 1 --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} Hands-on walkthroughs that teach OpenShell concepts by building real configurations. Each tutorial builds on the previous one, starting with core sandbox mechanics and progressing to production workflows. diff --git a/docs/tutorials/inference-ollama.md b/docs/tutorials/inference-ollama.md deleted file mode 100644 index 7bcc3dd41..000000000 --- a/docs/tutorials/inference-ollama.md +++ /dev/null @@ -1,221 +0,0 @@ ---- -title: - page: Inference with Ollama - nav: Inference with Ollama -description: Run local and cloud models inside an OpenShell sandbox using the Ollama community sandbox, or route sandbox requests to a host-level Ollama server. -topics: -- Generative AI -- Cybersecurity -tags: -- Tutorial -- Inference Routing -- Ollama -- Local Inference -- Sandbox -content: - type: tutorial - difficulty: technical_intermediate - audience: - - engineer ---- - - - -# Run Local Inference with Ollama - -This tutorial covers two ways to use Ollama with OpenShell: - -1. **Ollama sandbox (recommended)** — a self-contained sandbox with Ollama, Claude Code, and Codex pre-installed. One command to start. -2. **Host-level Ollama** — run Ollama on the gateway host and route sandbox inference to it. Useful when you want a single Ollama instance shared across multiple sandboxes. - -After completing this tutorial, you will know how to: - -- Launch the Ollama community sandbox for a batteries-included experience. -- Use `ollama launch` to start coding agents inside a sandbox. -- Expose a host-level Ollama server to sandboxes through `inference.local`. - -## Prerequisites - -- A working OpenShell installation. Complete the {doc}`/get-started/quickstart` before proceeding. - -## Option A: Ollama Community Sandbox (Recommended) - -The Ollama community sandbox bundles Ollama, Claude Code, OpenCode, and Codex into a single image. Ollama starts automatically when the sandbox launches. - -### Step 1: Create the Sandbox - -```console -$ openshell sandbox create --from ollama -``` - -This pulls the community sandbox image, applies the bundled policy, and drops you into a shell with Ollama running. - -::: - -### Step 2: Chat with a Model - -Chat with a local model - -```console -$ ollama run qwen3.5 -``` - -Or a cloud model - -```console -$ ollama run kimi-k2.5:cloud -``` - - -Or use `ollama launch` to start a coding agent with Ollama as the model backend: - -```console -$ ollama launch claude -$ ollama launch codex -$ ollama launch opencode -``` - -For CI/CD and automated workflows, `ollama launch` supports a headless mode: - -```console -$ ollama launch claude --yes --model qwen3.5 -``` - -### Model Recommendations - -| Use case | Model | Notes | -|---|---|---| -| Smoke test | `qwen3.5:0.8b` | Fast, lightweight, good for verifying setup | -| Coding and reasoning | `qwen3.5` | Strong tool calling support for agentic workflows | -| Complex tasks | `nemotron-3-super` | 122B parameter model, needs 48GB+ VRAM | -| No local GPU | `qwen3.5:cloud` | Runs on Ollama's cloud infrastructure, no `ollama pull` required | - -:::{note} -Cloud models use the `:cloud` tag suffix and do not require local hardware. - -```console -$ openshell sandbox create --from ollama -``` -::: - -### Tool Calling - -Agentic workflows (Claude Code, Codex, OpenCode) rely on tool calling. The following models have reliable tool calling support: Qwen 3.5, Nemotron-3-Super, GLM-5, and Kimi-K2.5. Check the [Ollama model library](https://ollama.com/library) for the latest models. - -### Updating Ollama - -To update Ollama inside a running sandbox: - -```console -$ update-ollama -``` - -Or auto-update on every sandbox start: - -```console -$ openshell sandbox create --from ollama -e OLLAMA_UPDATE=1 -``` - -## Option B: Host-Level Ollama - -Use this approach when you want a single Ollama instance on the gateway host, shared across multiple sandboxes through `inference.local`. - -:::{note} -This approach uses Ollama because it is easy to install and run locally, but you can substitute other inference engines such as vLLM, SGLang, TRT-LLM, and NVIDIA NIM by changing the startup command, base URL, and model name. -::: - -### Step 1: Install and Start Ollama - -Install [Ollama](https://ollama.com/) on the gateway host: - -```console -$ curl -fsSL https://ollama.com/install.sh | sh -``` - -Start Ollama on all interfaces so it is reachable from sandboxes: - -```console -$ OLLAMA_HOST=0.0.0.0:11434 ollama serve -``` - -:::{tip} -If you see `Error: listen tcp 0.0.0.0:11434: bind: address already in use`, Ollama is already running as a system service. Stop it first: - -```console -$ systemctl stop ollama -$ OLLAMA_HOST=0.0.0.0:11434 ollama serve -``` -::: - -### Step 2: Pull a Model - -In a second terminal, pull a model: - -```console -$ ollama run qwen3.5:0.8b -``` - -Type `/bye` to exit the interactive session. The model stays loaded. - -### Step 3: Create a Provider - -Create an OpenAI-compatible provider pointing at the host Ollama: - -```console -$ openshell provider create \ - --name ollama \ - --type openai \ - --credential OPENAI_API_KEY=empty \ - --config OPENAI_BASE_URL=http://host.openshell.internal:11434/v1 -``` - -OpenShell injects `host.openshell.internal` so sandboxes and the gateway can reach the host machine. You can also use the host's LAN IP. - -### Step 4: Set Inference Routing - -```console -$ openshell inference set --provider ollama --model qwen3.5:0.8b -``` - -Confirm: - -```console -$ openshell inference get -``` - -### Step 5: Verify from a Sandbox - -```console -$ openshell sandbox create -- \ - curl https://inference.local/v1/chat/completions \ - --json '{"messages":[{"role":"user","content":"hello"}],"max_tokens":10}' -``` - -The response should be JSON from the model. - -## Troubleshooting - -Common issues and fixes: - -- **Ollama not reachable from sandbox** — Ollama must be bound to `0.0.0.0`, not `127.0.0.1`. This applies to host-level Ollama only; the community sandbox handles this automatically. -- **`OPENAI_BASE_URL` wrong** — Use `http://host.openshell.internal:11434/v1`, not `localhost` or `127.0.0.1`. -- **Model not found** — Run `ollama ps` to confirm the model is loaded. Run `ollama pull ` if needed. -- **HTTPS vs HTTP** — Code inside sandboxes must call `https://inference.local`, not `http://`. -- **AMD GPU driver issues** — Ollama v0.18+ requires ROCm 7 drivers for AMD GPUs. Update your drivers if you see GPU detection failures. - -Useful commands: - -```console -$ openshell status -$ openshell inference get -$ openshell provider get ollama -``` - -## Next Steps - -- To learn more about managed inference, refer to {doc}`/inference/index`. -- To configure a different self-hosted backend, refer to {doc}`/inference/configure`. -- To explore more community sandboxes, refer to {doc}`/sandboxes/community-sandboxes`. diff --git a/fern/pages/tutorials/inference-ollama.mdx b/docs/tutorials/inference-ollama.mdx similarity index 96% rename from fern/pages/tutorials/inference-ollama.mdx rename to docs/tutorials/inference-ollama.mdx index 6d83bb4f8..5140038fc 100644 --- a/fern/pages/tutorials/inference-ollama.mdx +++ b/docs/tutorials/inference-ollama.mdx @@ -1,18 +1,12 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Run Local Inference with Ollama" sidebar-title: "Inference with Ollama" slug: "tutorials/inference-ollama" description: "Run local and cloud models inside an OpenShell sandbox using the Ollama community sandbox, or route sandbox requests to a host-level Ollama server." keywords: "Generative AI, Cybersecurity, Tutorial, Inference Routing, Ollama, Local Inference, Sandbox" -tags: - - Tutorial - - Inference Routing - - Ollama - - Local Inference - - Sandbox --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} This tutorial covers two ways to use Ollama with OpenShell: diff --git a/docs/tutorials/local-inference-lmstudio.md b/docs/tutorials/local-inference-lmstudio.md deleted file mode 100644 index aec59ed24..000000000 --- a/docs/tutorials/local-inference-lmstudio.md +++ /dev/null @@ -1,222 +0,0 @@ ---- -title: - page: Route Local Inference Requests to LM Studio - nav: Local Inference with LM Studio -description: Configure inference.local to route sandbox requests to a local LM Studio server running on the gateway host. -topics: -- Generative AI -- Cybersecurity -tags: -- Tutorial -- Inference Routing -- LM Studio -- Local Inference -- Sandbox -content: - type: tutorial - difficulty: technical_intermediate - audience: - - engineer ---- - - - -# Route Local Inference Requests to LM Studio - -This tutorial describes how to configure OpenShell to route inference requests to a local LM Studio server. - -:::{note} -The LM Studio server provides easy setup with both OpenAI and Anthropic compatible endpoints. -::: - -This tutorial will cover: - -- Expose a local inference server to OpenShell sandboxes. -- Verify end-to-end inference from inside a sandbox. - -## Prerequisites - -First, complete OpenShell installation and follow the {doc}`/get-started/quickstart`. - -[Install the LM Studio app](https://lmstudio.ai/download). Make sure that your LM Studio is running in the same environment as your gateway. - -If you prefer to work without having to keep the LM Studio app open, download llmster (headless LM Studio) with the following command: - -### Linux/Mac -```console -$ curl -fsSL https://lmstudio.ai/install.sh | bash -``` - -### Windows -```console -$ irm https://lmstudio.ai/install.ps1 | iex -``` - -And start llmster: -```console -$ lms daemon up -``` - -## Step 1: Start LM Studio Local Server - -Start the LM Studio local server from the Developer tab, and verify the OpenAI-compatible endpoint is enabled. - -LM Studio will listen to `127.0.0.1:1234` by default. For use with OpenShell, you'll need to configure LM Studio to listen on all interfaces (`0.0.0.0`). - -If you're using the GUI, go to the Developer Tab, select Server Settings, then enable Serve on Local Network. - -If you're using llmster in headless mode, run `lms server start --bind 0.0.0.0`. - -## Step 2: Test with a small model - -In the LM Studio app, head to the Model Search tab to download a small model like Qwen3.5 2B. - -In the terminal, use the following command to download and load the model: -```console -$ lms get qwen/qwen3.5-2b -$ lms load qwen/qwen3.5-2b -``` - - -## Step 3: Add LM Studio as a provider - -Choose the provider type that matches the client protocol you want to route through `inference.local`. - -:::::{tab-set} - -::::{tab-item} OpenAI-compatible - -Add LM Studio as an OpenAI-compatible provider through `host.openshell.internal`: - -```console -$ openshell provider create \ - --name lmstudio \ - --type openai \ - --credential OPENAI_API_KEY=lmstudio \ - --config OPENAI_BASE_URL=http://host.openshell.internal:1234/v1 -``` - -Use this provider for clients that send OpenAI-compatible requests such as `POST /v1/chat/completions` or `POST /v1/responses`. - -:::: - -::::{tab-item} Anthropic-compatible - -Add a provider that points to LM Studio's Anthropic-compatible `POST /v1/messages` endpoint: - -```console -$ openshell provider create \ - --name lmstudio-anthropic \ - --type anthropic \ - --credential ANTHROPIC_API_KEY=lmstudio \ - --config ANTHROPIC_BASE_URL=http://host.openshell.internal:1234 -``` - -Use this provider for Anthropic-compatible `POST /v1/messages` requests. - -:::: - -::::: - - -## Step 4: Configure LM Studio as the local inference provider - -Set the managed inference route for the active gateway: - -:::::{tab-set} - -::::{tab-item} OpenAI-compatible - -```console -$ openshell inference set --provider lmstudio --model qwen/qwen3.5-2b -``` - -If the command succeeds, OpenShell has verified that the upstream is reachable and accepts the expected OpenAI-compatible request shape. - -:::: - -::::{tab-item} Anthropic-compatible - -```console -$ openshell inference set --provider lmstudio-anthropic --model qwen/qwen3.5-2b -``` - -If the command succeeds, OpenShell has verified that the upstream is reachable and accepts the expected Anthropic-compatible request shape. - -:::: - -::::: - -The active `inference.local` route is gateway-scoped, so only one provider and model pair is active at a time. Re-run `openshell inference set` whenever you want to switch between OpenAI-compatible and Anthropic-compatible clients. - -Confirm the saved config: - -```console -$ openshell inference get -``` - -You should see either `Provider: lmstudio` or `Provider: lmstudio-anthropic`, along with `Model: qwen/qwen3.5-2b`. - -## Step 5: Verify from Inside a Sandbox - -Run a simple request through `https://inference.local`: - -:::::{tab-set} - -::::{tab-item} OpenAI-compatible - -```bash -openshell sandbox create -- \ - curl https://inference.local/v1/chat/completions \ - --json '{"messages":[{"role":"user","content":"hello"}],"max_tokens":10}' - -openshell sandbox create -- \ - curl https://inference.local/v1/responses \ - --json '{ - "instructions": "You are a helpful assistant.", - "input": "hello", - "max_output_tokens": 10 - }' -``` - -:::: - -::::{tab-item} Anthropic-compatible - -```bash -openshell sandbox create -- \ - curl https://inference.local/v1/messages \ - --json '{"messages":[{"role":"user","content":"hello"}],"max_tokens":10}' -``` - -:::: - -::::: - -## Troubleshooting - -If setup fails, check these first: - -- LM Studio local server is running and reachable from the gateway host -- `OPENAI_BASE_URL` uses `http://host.openshell.internal:1234/v1` when you use an `openai` provider -- `ANTHROPIC_BASE_URL` uses `http://host.openshell.internal:1234` when you use an `anthropic` provider -- The gateway and LM Studio run on the same machine or a reachable network path -- The configured model name matches the model exposed by LM Studio - -Useful commands: - -```console -$ openshell status -$ openshell inference get -$ openshell provider get lmstudio -$ openshell provider get lmstudio-anthropic -``` - -## Next Steps - -- To learn more about using the LM Studio CLI, refer to [LM Studio docs](https://lmstudio.ai/docs/cli) -- To learn more about managed inference, refer to {doc}`/inference/index`. -- To configure a different self-hosted backend, refer to {doc}`/inference/configure`. diff --git a/fern/pages/tutorials/local-inference-lmstudio.mdx b/docs/tutorials/local-inference-lmstudio.mdx similarity index 96% rename from fern/pages/tutorials/local-inference-lmstudio.mdx rename to docs/tutorials/local-inference-lmstudio.mdx index bea4e8cad..fec37fe2d 100644 --- a/fern/pages/tutorials/local-inference-lmstudio.mdx +++ b/docs/tutorials/local-inference-lmstudio.mdx @@ -1,18 +1,12 @@ --- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 title: "Route Local Inference Requests to LM Studio" sidebar-title: "Local Inference with LM Studio" slug: "tutorials/local-inference-lmstudio" description: "Configure inference.local to route sandbox requests to a local LM Studio server running on the gateway host." keywords: "Generative AI, Cybersecurity, Tutorial, Inference Routing, LM Studio, Local Inference, Sandbox" -tags: - - Tutorial - - Inference Routing - - LM Studio - - Local Inference - - Sandbox --- -{/* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 */} This tutorial describes how to configure OpenShell to route inference requests to a local LM Studio server. diff --git a/fern/assets/openshell-terminal.png b/fern/assets/openshell-terminal.png deleted file mode 100644 index 09fe2b768..000000000 Binary files a/fern/assets/openshell-terminal.png and /dev/null differ diff --git a/fern/docs.yml b/fern/docs.yml index d1dab6e7e..010c51679 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -56,17 +56,17 @@ navbar-links: experimental: mdx-components: - - ./components + - ../docs/_components basepath-aware: true versions: - display-name: Latest - path: versions/latest.yml + path: ../docs/index.yml slug: latest redirects: # Paths are relative to the site root; subpath prefix matches instances + custom-domain. - # Sphinx-style URLs used .../path/to/page/index.html; Fern canonical URLs omit index.html. + # Legacy HTML URLs used .../path/to/page/index.html; Fern canonical URLs omit index.html. # List explicit /index.html routes before :path*/index.html so empty path segments do not # mis-resolve (see NeMo-Curator fern/docs.yml). :path*.html must follow :path*/index.html rules. # https://www.buildwithfern.com/learn/docs/configuration/site-level-settings#redirects-configuration diff --git a/mise.toml b/mise.toml index 4bcb4e072..12f67fdec 100644 --- a/mise.toml +++ b/mise.toml @@ -15,6 +15,7 @@ python.precompiled_flavor = "install_only_stripped" [tools] python = "3.13.12" rust = "stable" +node = "24" kubectl = "1.35.1" uv = "0.10.2" protoc = "29.6" diff --git a/pyproject.toml b/pyproject.toml index 899885929..71ceed34f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,15 +53,6 @@ dev = [ "grpcio-tools>=1.60", "pyelftools>=0.30", ] -docs = [ - "sphinx<=7.5", - "myst-parser<=5", - "sphinx-copybutton<=0.6", - "sphinx-design", - "sphinx-autobuild", - "sphinxcontrib-mermaid", - "nvidia-sphinx-theme", -] [tool.uv] # Don't try to install the root package with uv sync - use uv pip install . instead diff --git a/tasks/docs.toml b/tasks/docs.toml index 5dd1897d4..bc9534fa0 100644 --- a/tasks/docs.toml +++ b/tasks/docs.toml @@ -4,32 +4,34 @@ # Documentation build, serve, and check tasks ["docs"] -description = "Build HTML documentation" -run = """ -if ! uv run sphinx-build -b html docs _build/docs; then - echo "" - echo "Build failed. If this is a dependency issue, try: mise run docs:deps" - exit 1 -fi -echo "" -echo "Documentation built successfully." -echo "file://$(pwd)/_build/docs/index.html" -""" +description = "Validate Fern documentation" +depends = ["docs:build:strict"] ["docs:deps"] -description = "Install documentation dependencies" -run = "uv sync --group docs" +description = "Resolve Fern CLI for docs tasks" +run = """ +FERN_VERSION=$(node -p "require('./fern/fern.config.json').version") +npx --yes "fern-api@${FERN_VERSION}" --version +""" ["docs:build:strict"] -description = "Build HTML documentation with warnings as errors" +description = "Validate docs config and links" depends = ["docs:deps"] -run = "uv run sphinx-build -b html -W --keep-going docs _build/docs" +run = """ +FERN_VERSION=$(node -p "require('./fern/fern.config.json').version") +cd fern +npx --yes "fern-api@${FERN_VERSION}" check +""" ["docs:serve"] -description = "Serve documentation with live reload on port 8000" +description = "Serve Fern docs locally" depends = ["docs:deps"] -run = "uv run sphinx-autobuild docs _build/docs --port 8000 --open-browser" +run = """ +FERN_VERSION=$(node -p "require('./fern/fern.config.json').version") +cd fern +npx --yes "fern-api@${FERN_VERSION}" docs dev +""" ["docs:clean"] -description = "Remove built documentation" -run = "rm -rf _build/docs" +description = "Remove local Fern docs cache" +run = "rm -rf .fern-cache fern/.fern-cache" diff --git a/uv.lock b/uv.lock index 3869daf05..853b8eb03 100644 --- a/uv.lock +++ b/uv.lock @@ -2,140 +2,6 @@ version = 1 revision = 3 requires-python = ">=3.12" -[[package]] -name = "accessible-pygments" -version = "0.0.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, -] - -[[package]] -name = "alabaster" -version = "0.7.16" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776, upload-time = "2024-01-10T00:56:10.189Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511, upload-time = "2024-01-10T00:56:08.388Z" }, -] - -[[package]] -name = "anyio" -version = "4.12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, -] - -[[package]] -name = "babel" -version = "2.18.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, -] - -[[package]] -name = "beautifulsoup4" -version = "4.14.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "soupsieve" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, -] - -[[package]] -name = "certifi" -version = "2026.2.25" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, - { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, - { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, - { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, - { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, - { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, - { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, -] - -[[package]] -name = "click" -version = "8.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, -] - [[package]] name = "cloudpickle" version = "3.1.2" @@ -228,15 +94,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, ] -[[package]] -name = "docutils" -version = "0.21.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, -] - [[package]] name = "execnet" version = "2.1.2" @@ -330,33 +187,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/d1/909e6a05bfd44d46327dc4b8a78beb2bae4fb245ffab2772e350081aaf7e/grpcio_tools-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d58ade518b546120ec8f0a8e006fc8076ae5df151250ebd7e82e9b5e152c229", size = 1190196, upload-time = "2026-02-06T09:59:28.359Z" }, ] -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, -] - -[[package]] -name = "imagesize" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -366,93 +196,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, -] - -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, -] - [[package]] name = "maturin" version = "1.11.5" @@ -474,56 +217,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/67/c94f8f5440bc42d54113a2d99de0d6107f06b5a33f31823e52b2715d856f/maturin-1.11.5-py3-none-win_arm64.whl", hash = "sha256:9348f7f0a346108e0c96e6719be91da4470bd43c15802435e9f4157f5cca43d4", size = 7624029, upload-time = "2026-01-09T11:06:08.728Z" }, ] -[[package]] -name = "mdit-py-plugins" -version = "0.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, -] - -[[package]] -name = "myst-parser" -version = "4.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docutils" }, - { name = "jinja2" }, - { name = "markdown-it-py" }, - { name = "mdit-py-plugins" }, - { name = "pyyaml" }, - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" }, -] - -[[package]] -name = "nvidia-sphinx-theme" -version = "0.0.9.post1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydata-sphinx-theme" }, - { name = "sphinx" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/79/017fab2f7167a9a9795665f894d04f77aafceca80821b51589bb4b23ff5c/nvidia_sphinx_theme-0.0.9.post1-py3-none-any.whl", hash = "sha256:21ca60206dff2f380d7783d64bbaf71a5b9cacae53c7d0686f089c16b5a3d45a", size = 143816, upload-time = "2025-11-09T23:16:55.719Z" }, -] - [[package]] name = "openshell" source = { virtual = "." } @@ -546,15 +239,6 @@ dev = [ { name = "setuptools-scm" }, { name = "ty" }, ] -docs = [ - { name = "myst-parser" }, - { name = "nvidia-sphinx-theme" }, - { name = "sphinx" }, - { name = "sphinx-autobuild" }, - { name = "sphinx-copybutton" }, - { name = "sphinx-design" }, - { name = "sphinxcontrib-mermaid" }, -] [package.metadata] requires-dist = [ @@ -576,15 +260,6 @@ dev = [ { name = "setuptools-scm", specifier = ">=8" }, { name = "ty", specifier = ">=0.0.1a6" }, ] -docs = [ - { name = "myst-parser", specifier = "<=5" }, - { name = "nvidia-sphinx-theme" }, - { name = "sphinx", specifier = "<=7.5" }, - { name = "sphinx-autobuild" }, - { name = "sphinx-copybutton", specifier = "<=0.6" }, - { name = "sphinx-design" }, - { name = "sphinxcontrib-mermaid" }, -] [[package]] name = "packaging" @@ -619,24 +294,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, ] -[[package]] -name = "pydata-sphinx-theme" -version = "0.16.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "accessible-pygments" }, - { name = "babel" }, - { name = "beautifulsoup4" }, - { name = "docutils" }, - { name = "pygments" }, - { name = "sphinx" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/00/20/bb50f9de3a6de69e6abd6b087b52fa2418a0418b19597601605f855ad044/pydata_sphinx_theme-0.16.1.tar.gz", hash = "sha256:a08b7f0b7f70387219dc659bff0893a7554d5eb39b59d3b8ef37b8401b7642d7", size = 2412693, upload-time = "2024-12-17T10:53:39.537Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/0d/8ba33fa83a7dcde13eb3c1c2a0c1cc29950a048bfed6d9b0d8b6bd710b4c/pydata_sphinx_theme-0.16.1-py3-none-any.whl", hash = "sha256:225331e8ac4b32682c18fcac5a57a6f717c4e632cea5dd0e247b55155faeccde", size = 6723264, upload-time = "2024-12-17T10:53:35.645Z" }, -] - [[package]] name = "pyelftools" version = "0.32" @@ -711,67 +368,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - -[[package]] -name = "requests" -version = "2.32.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, -] - [[package]] name = "ruff" version = "0.14.14" @@ -820,173 +416,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3d/ea/ac2bf868899d0d2e82ef72d350d97a846110c709bacf2d968431576ca915/setuptools_scm-9.2.2-py3-none-any.whl", hash = "sha256:30e8f84d2ab1ba7cb0e653429b179395d0c33775d54807fc5f1dd6671801aef7", size = 62975, upload-time = "2025-10-19T22:08:04.007Z" }, ] -[[package]] -name = "snowballstemmer" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, -] - -[[package]] -name = "soupsieve" -version = "2.8.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, -] - -[[package]] -name = "sphinx" -version = "7.4.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils" }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911, upload-time = "2024-07-20T14:46:56.059Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624, upload-time = "2024-07-20T14:46:52.142Z" }, -] - -[[package]] -name = "sphinx-autobuild" -version = "2025.8.25" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama" }, - { name = "sphinx" }, - { name = "starlette" }, - { name = "uvicorn" }, - { name = "watchfiles" }, - { name = "websockets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e0/3c/a59a3a453d4133777f7ed2e83c80b7dc817d43c74b74298ca0af869662ad/sphinx_autobuild-2025.8.25.tar.gz", hash = "sha256:9cf5aab32853c8c31af572e4fecdc09c997e2b8be5a07daf2a389e270e85b213", size = 15200, upload-time = "2025-08-25T18:44:55.436Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/20/56411b52f917696995f5ad27d2ea7e9492c84a043c5b49a3a3173573cd93/sphinx_autobuild-2025.8.25-py3-none-any.whl", hash = "sha256:b750ac7d5a18603e4665294323fd20f6dcc0a984117026d1986704fa68f0379a", size = 12535, upload-time = "2025-08-25T18:44:54.164Z" }, -] - -[[package]] -name = "sphinx-copybutton" -version = "0.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039, upload-time = "2023-04-14T08:10:22.998Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343, upload-time = "2023-04-14T08:10:20.844Z" }, -] - -[[package]] -name = "sphinx-design" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/13/7b/804f311da4663a4aecc6cf7abd83443f3d4ded970826d0c958edc77d4527/sphinx_design-0.7.0.tar.gz", hash = "sha256:d2a3f5b19c24b916adb52f97c5f00efab4009ca337812001109084a740ec9b7a", size = 2203582, upload-time = "2026-01-19T13:12:53.297Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/30/cf/45dd359f6ca0c3762ce0490f681da242f0530c49c81050c035c016bfdd3a/sphinx_design-0.7.0-py3-none-any.whl", hash = "sha256:f82bf179951d58f55dca78ab3706aeafa496b741a91b1911d371441127d64282", size = 2220350, upload-time = "2026-01-19T13:12:51.077Z" }, -] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, -] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, -] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, -] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, -] - -[[package]] -name = "sphinxcontrib-mermaid" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "pyyaml" }, - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/a5/65a5c439cc14ba80483b9891e9350f11efb80cd3bdccb222f0c738068c78/sphinxcontrib_mermaid-2.0.0.tar.gz", hash = "sha256:cf4f7d453d001132eaba5d1fdf53d42049f02e913213cf8337427483bfca26f4", size = 18194, upload-time = "2026-01-13T17:13:42.563Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/de/bd96c69b62e967bffd02c6d89dfca9471b04e761c466725fc39746abf41d/sphinxcontrib_mermaid-2.0.0-py3-none-any.whl", hash = "sha256:59a73249bbee2c74b1a4db036f8e8899ade65982bdda6712cf22b4f4e9874bb5", size = 14055, upload-time = "2026-01-13T17:13:41.481Z" }, -] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, -] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, -] - -[[package]] -name = "starlette" -version = "0.52.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, -] - [[package]] name = "ty" version = "0.0.14" @@ -1019,140 +448,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac8 wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] - -[[package]] -name = "urllib3" -version = "2.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, -] - -[[package]] -name = "uvicorn" -version = "0.41.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, -] - -[[package]] -name = "watchfiles" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, - { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, - { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, - { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, - { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, - { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, - { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, - { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, - { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, - { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, - { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, - { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, - { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, - { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, - { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, - { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, - { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, - { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, - { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, - { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, - { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, - { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, - { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, - { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, - { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, - { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, - { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, - { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, - { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, - { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, - { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, - { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, - { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, - { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, - { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, - { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, - { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, - { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, - { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, - { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, - { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, - { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, -] - -[[package]] -name = "websockets" -version = "16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, - { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, - { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, - { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, - { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, - { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, - { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, - { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, - { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, - { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, - { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, - { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, - { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, - { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, - { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, - { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, - { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, - { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, -]