From 37b8671dbb873d30ddf006af45780ecc216ef660 Mon Sep 17 00:00:00 2001 From: Dan Barr <6922515+danbarr@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:16:04 -0400 Subject: [PATCH 1/3] Add Stacklok Enterprise platform documentation Introduce a new docs/platform/ section documenting the Stacklok Enterprise distribution alongside the existing OSS ToolHive docs. New content areas: - Enterprise platform: deployment, identity config, registry server config, distributed and airgapped installs, artifact verification - Enterprise Manager: configuration, degraded mode, and policy reference (build env, CA certificate, desktop app, registry, telemetry, non-registry servers) - Stacklok Desktop: rollout, policy enforcement, and deep links - Enterprise Cloud UI: catalog browsing, administration, AI assistant, and configuration - Enterprise authorization: intro, namespace self-service, and an Entra ID quickstart - Enterprise CLI and a new AI gateway landing page Reference and tooling: - Add enterprise CRD reference pages and schemas (ClusterPlatformRole, ClusterPlatformRoleBinding, PlatformRoleBinding, ToolHiveAuthorizationPolicy) - Extend the CRD reference generator to support enterprise CRD sets - Update the homepage product grid, styles, and icons for the platform layout - Update sidebars, Docusaurus config, and Vercel redirects Also refresh several OSS reference and guide pages and the upstream-release docs tooling. Signed-off-by: Dan Barr <6922515+danbarr@users.noreply.github.com> --- .claude/agents/mcp-guide-writer.md | 2 +- .claude/skills/upstream-release-docs/SKILL.md | 21 +- .github/upstream-projects.yaml | 54 +- .github/workflows/upstream-release-docs.yml | 96 +- AGENTS.md | 7 +- STYLE-GUIDE.md | 35 +- docs/ai-gateway/index.mdx | 41 + docs/platform/enterprise-authz/index.mdx | 32 + docs/platform/enterprise-authz/intro.mdx | 137 +++ .../namespace-self-service.mdx | 168 ++++ .../enterprise-authz/quickstart-entra.mdx | 541 +++++++++++ docs/platform/enterprise-cli/index.mdx | 116 +++ .../administration/entries.mdx | 87 ++ .../administration/index.mdx | 37 + .../administration/registries.mdx | 76 ++ .../administration/sources.mdx | 104 +++ .../enterprise-cloud-ui/ai-assistant.mdx | 96 ++ .../enterprise-cloud-ui/browse-catalog.mdx | 143 +++ .../enterprise-cloud-ui/configure.mdx | 306 +++++++ docs/platform/enterprise-cloud-ui/index.mdx | 29 + docs/platform/enterprise-cloud-ui/intro.mdx | 114 +++ .../enterprise-desktop/deep-links.mdx | 56 ++ docs/platform/enterprise-desktop/index.mdx | 32 + docs/platform/enterprise-desktop/intro.mdx | 85 ++ .../enterprise-desktop/policy-enforcement.mdx | 90 ++ docs/platform/enterprise-desktop/rollout.mdx | 350 +++++++ .../platform/enterprise-manager/configure.mdx | 115 +++ .../enterprise-manager/degraded-mode.mdx | 71 ++ docs/platform/enterprise-manager/index.mdx | 28 + docs/platform/enterprise-manager/intro.mdx | 89 ++ .../enterprise-manager/policies/build-env.mdx | 63 ++ .../policies/ca-certificate.mdx | 69 ++ .../policies/desktop-app.mdx | 110 +++ .../enterprise-manager/policies/index.mdx | 70 ++ .../policies/non-registry-servers.mdx | 82 ++ .../enterprise-manager/policies/registry.mdx | 78 ++ .../enterprise-manager/policies/telemetry.mdx | 100 ++ .../enterprise-platform/airgap-install.mdx | 633 +++++++++++++ .../configure-identity.mdx | 255 ++++++ .../configure-registry-server.mdx | 145 +++ .../enterprise-platform/deployment.mdx | 335 +++++++ .../distributed-deployments.mdx | 232 +++++ docs/platform/enterprise-platform/index.mdx | 89 ++ .../enterprise-platform/verify-artifacts.mdx | 344 +++++++ .../enterprise.mdx => platform/index.mdx} | 136 +-- .../reference/crds/clusterplatformrole.mdx | 35 + .../crds/clusterplatformrolebinding.mdx | 36 + docs/platform/reference/crds/index.mdx | 54 ++ .../reference/crds/platformrolebinding.mdx | 38 + .../crds/toolhiveauthorizationpolicy.mdx | 33 + docs/theme-preview.mdx | 3 +- docs/toolhive/concepts/auth-framework.mdx | 9 + docs/toolhive/concepts/cedar-policies.mdx | 9 + docs/toolhive/faq.mdx | 38 +- docs/toolhive/guides-cli/quickstart.mdx | 2 +- docs/toolhive/guides-cloud-ui/intro.mdx | 2 +- docs/toolhive/guides-k8s/quickstart.mdx | 2 +- docs/toolhive/guides-registry/intro.mdx | 2 + docs/toolhive/guides-registry/quickstart.mdx | 2 +- docs/toolhive/guides-ui/quickstart.mdx | 2 +- docs/toolhive/index.mdx | 10 +- .../reference/crds/embeddingserver.mdx | 1 - docs/toolhive/reference/crds/index.mdx | 6 +- .../reference/crds/mcpauthzconfig.mdx | 1 - .../reference/crds/mcpexternalauthconfig.mdx | 1 - docs/toolhive/reference/crds/mcpgroup.mdx | 1 - .../toolhive/reference/crds/mcpoidcconfig.mdx | 1 - docs/toolhive/reference/crds/mcpregistry.mdx | 1 - .../reference/crds/mcpremoteproxy.mdx | 1 - docs/toolhive/reference/crds/mcpserver.mdx | 1 - .../reference/crds/mcpserverentry.mdx | 1 - .../reference/crds/mcptelemetryconfig.mdx | 1 - .../toolhive/reference/crds/mcptoolconfig.mdx | 1 - .../reference/crds/mcpwebhookconfig.mdx | 1 - .../virtualmcpcompositetooldefinition.mdx | 1 - .../reference/crds/virtualmcpserver.mdx | 1 - docs/toolhive/reference/index.mdx | 1 - .../reference/registry-schema-upstream.mdx | 1 - docs/toolhive/support.mdx | 35 +- docusaurus.config.ts | 83 +- plugins/crd-reference-remark/index.mjs | 36 +- scripts/extract-crd-schemas.mjs | 15 +- scripts/generate-crd-barrel.mjs | 111 +++ scripts/generate-crd-pages.mjs | 173 ++-- scripts/lib/crd-intros.mjs | 46 +- scripts/lib/crd-sets.mjs | 82 ++ scripts/upstream-release/extract-crds.mjs | 167 ++++ sidebars.ts | 865 ++++++++++-------- src/components/CRDReference/all-schemas.ts | 51 ++ src/components/CRDReference/index.tsx | 5 +- src/components/CRDReference/schemas.ts | 40 - src/components/ProductGrid/index.tsx | 3 +- src/components/ProductGrid/styles.module.css | 4 + src/css/custom.css | 49 + src/pages/index.tsx | 98 +- .../clusterplatformrolebindings.example.yaml | 11 + .../clusterplatformrolebindings.schema.json | 173 ++++ .../clusterplatformroles.example.yaml | 9 + .../clusterplatformroles.schema.json | 138 +++ static/api-specs/enterprise-crds/index.json | 102 +++ .../platformrolebindings.example.yaml | 12 + .../platformrolebindings.schema.json | 173 ++++ static/api-specs/enterprise-crds/sidebar.json | 23 + ...toolhiveauthorizationpolicies.example.yaml | 8 + .../toolhiveauthorizationpolicies.schema.json | 318 +++++++ .../embeddingservers.example.yaml | 0 .../embeddingservers.schema.json | 0 .../{crds => toolhive-crds}/index.json | 0 .../mcpauthzconfigs.example.yaml | 0 .../mcpauthzconfigs.schema.json | 0 .../mcpexternalauthconfigs.example.yaml | 0 .../mcpexternalauthconfigs.schema.json | 0 .../mcpgroups.example.yaml | 0 .../mcpgroups.schema.json | 0 .../mcpoidcconfigs.example.yaml | 0 .../mcpoidcconfigs.schema.json | 0 .../mcpregistries.example.yaml | 0 .../mcpregistries.schema.json | 0 .../mcpremoteproxies.example.yaml | 0 .../mcpremoteproxies.schema.json | 0 .../mcpserverentries.example.yaml | 0 .../mcpserverentries.schema.json | 0 .../mcpservers.example.yaml | 0 .../mcpservers.schema.json | 0 .../mcptelemetryconfigs.example.yaml | 0 .../mcptelemetryconfigs.schema.json | 0 .../mcptoolconfigs.example.yaml | 0 .../mcptoolconfigs.schema.json | 0 .../mcpwebhookconfigs.example.yaml | 0 .../mcpwebhookconfigs.schema.json | 0 .../{crds => toolhive-crds}/sidebar.json | 4 +- ...almcpcompositetooldefinitions.example.yaml | 0 ...ualmcpcompositetooldefinitions.schema.json | 0 .../virtualmcpservers.example.yaml | 0 .../virtualmcpservers.schema.json | 0 ...cklok-website-icons-circuit-dark-green.svg | 67 ++ ...klok-website-icons-circuit-light-green.svg | 67 ++ ...ok-website-icons-efficiency-dark-green.svg | 0 ...k-website-icons-efficiency-light-green.svg | 0 ...klok-website-icons-security-dark-green.svg | 40 + ...lok-website-icons-security-light-green.svg | 40 + vercel.json | 5 + 142 files changed, 8513 insertions(+), 738 deletions(-) create mode 100644 docs/ai-gateway/index.mdx create mode 100644 docs/platform/enterprise-authz/index.mdx create mode 100644 docs/platform/enterprise-authz/intro.mdx create mode 100644 docs/platform/enterprise-authz/namespace-self-service.mdx create mode 100644 docs/platform/enterprise-authz/quickstart-entra.mdx create mode 100644 docs/platform/enterprise-cli/index.mdx create mode 100644 docs/platform/enterprise-cloud-ui/administration/entries.mdx create mode 100644 docs/platform/enterprise-cloud-ui/administration/index.mdx create mode 100644 docs/platform/enterprise-cloud-ui/administration/registries.mdx create mode 100644 docs/platform/enterprise-cloud-ui/administration/sources.mdx create mode 100644 docs/platform/enterprise-cloud-ui/ai-assistant.mdx create mode 100644 docs/platform/enterprise-cloud-ui/browse-catalog.mdx create mode 100644 docs/platform/enterprise-cloud-ui/configure.mdx create mode 100644 docs/platform/enterprise-cloud-ui/index.mdx create mode 100644 docs/platform/enterprise-cloud-ui/intro.mdx create mode 100644 docs/platform/enterprise-desktop/deep-links.mdx create mode 100644 docs/platform/enterprise-desktop/index.mdx create mode 100644 docs/platform/enterprise-desktop/intro.mdx create mode 100644 docs/platform/enterprise-desktop/policy-enforcement.mdx create mode 100644 docs/platform/enterprise-desktop/rollout.mdx create mode 100644 docs/platform/enterprise-manager/configure.mdx create mode 100644 docs/platform/enterprise-manager/degraded-mode.mdx create mode 100644 docs/platform/enterprise-manager/index.mdx create mode 100644 docs/platform/enterprise-manager/intro.mdx create mode 100644 docs/platform/enterprise-manager/policies/build-env.mdx create mode 100644 docs/platform/enterprise-manager/policies/ca-certificate.mdx create mode 100644 docs/platform/enterprise-manager/policies/desktop-app.mdx create mode 100644 docs/platform/enterprise-manager/policies/index.mdx create mode 100644 docs/platform/enterprise-manager/policies/non-registry-servers.mdx create mode 100644 docs/platform/enterprise-manager/policies/registry.mdx create mode 100644 docs/platform/enterprise-manager/policies/telemetry.mdx create mode 100644 docs/platform/enterprise-platform/airgap-install.mdx create mode 100644 docs/platform/enterprise-platform/configure-identity.mdx create mode 100644 docs/platform/enterprise-platform/configure-registry-server.mdx create mode 100644 docs/platform/enterprise-platform/deployment.mdx create mode 100644 docs/platform/enterprise-platform/distributed-deployments.mdx create mode 100644 docs/platform/enterprise-platform/index.mdx create mode 100644 docs/platform/enterprise-platform/verify-artifacts.mdx rename docs/{toolhive/enterprise.mdx => platform/index.mdx} (67%) create mode 100644 docs/platform/reference/crds/clusterplatformrole.mdx create mode 100644 docs/platform/reference/crds/clusterplatformrolebinding.mdx create mode 100644 docs/platform/reference/crds/index.mdx create mode 100644 docs/platform/reference/crds/platformrolebinding.mdx create mode 100644 docs/platform/reference/crds/toolhiveauthorizationpolicy.mdx create mode 100644 scripts/generate-crd-barrel.mjs create mode 100644 scripts/lib/crd-sets.mjs create mode 100644 scripts/upstream-release/extract-crds.mjs create mode 100644 src/components/CRDReference/all-schemas.ts delete mode 100644 src/components/CRDReference/schemas.ts create mode 100644 static/api-specs/enterprise-crds/clusterplatformrolebindings.example.yaml create mode 100644 static/api-specs/enterprise-crds/clusterplatformrolebindings.schema.json create mode 100644 static/api-specs/enterprise-crds/clusterplatformroles.example.yaml create mode 100644 static/api-specs/enterprise-crds/clusterplatformroles.schema.json create mode 100644 static/api-specs/enterprise-crds/index.json create mode 100644 static/api-specs/enterprise-crds/platformrolebindings.example.yaml create mode 100644 static/api-specs/enterprise-crds/platformrolebindings.schema.json create mode 100644 static/api-specs/enterprise-crds/sidebar.json create mode 100644 static/api-specs/enterprise-crds/toolhiveauthorizationpolicies.example.yaml create mode 100644 static/api-specs/enterprise-crds/toolhiveauthorizationpolicies.schema.json rename static/api-specs/{crds => toolhive-crds}/embeddingservers.example.yaml (100%) rename static/api-specs/{crds => toolhive-crds}/embeddingservers.schema.json (100%) rename static/api-specs/{crds => toolhive-crds}/index.json (100%) rename static/api-specs/{crds => toolhive-crds}/mcpauthzconfigs.example.yaml (100%) rename static/api-specs/{crds => toolhive-crds}/mcpauthzconfigs.schema.json (100%) rename static/api-specs/{crds => toolhive-crds}/mcpexternalauthconfigs.example.yaml (100%) rename static/api-specs/{crds => toolhive-crds}/mcpexternalauthconfigs.schema.json (100%) rename static/api-specs/{crds => toolhive-crds}/mcpgroups.example.yaml (100%) rename static/api-specs/{crds => toolhive-crds}/mcpgroups.schema.json (100%) rename static/api-specs/{crds => toolhive-crds}/mcpoidcconfigs.example.yaml (100%) rename static/api-specs/{crds => toolhive-crds}/mcpoidcconfigs.schema.json (100%) rename static/api-specs/{crds => toolhive-crds}/mcpregistries.example.yaml (100%) rename static/api-specs/{crds => toolhive-crds}/mcpregistries.schema.json (100%) rename static/api-specs/{crds => toolhive-crds}/mcpremoteproxies.example.yaml (100%) rename static/api-specs/{crds => toolhive-crds}/mcpremoteproxies.schema.json (100%) rename static/api-specs/{crds => toolhive-crds}/mcpserverentries.example.yaml (100%) rename static/api-specs/{crds => toolhive-crds}/mcpserverentries.schema.json (100%) rename static/api-specs/{crds => toolhive-crds}/mcpservers.example.yaml (100%) rename static/api-specs/{crds => toolhive-crds}/mcpservers.schema.json (100%) rename static/api-specs/{crds => toolhive-crds}/mcptelemetryconfigs.example.yaml (100%) rename static/api-specs/{crds => toolhive-crds}/mcptelemetryconfigs.schema.json (100%) rename static/api-specs/{crds => toolhive-crds}/mcptoolconfigs.example.yaml (100%) rename static/api-specs/{crds => toolhive-crds}/mcptoolconfigs.schema.json (100%) rename static/api-specs/{crds => toolhive-crds}/mcpwebhookconfigs.example.yaml (100%) rename static/api-specs/{crds => toolhive-crds}/mcpwebhookconfigs.schema.json (100%) rename static/api-specs/{crds => toolhive-crds}/sidebar.json (89%) rename static/api-specs/{crds => toolhive-crds}/virtualmcpcompositetooldefinitions.example.yaml (100%) rename static/api-specs/{crds => toolhive-crds}/virtualmcpcompositetooldefinitions.schema.json (100%) rename static/api-specs/{crds => toolhive-crds}/virtualmcpservers.example.yaml (100%) rename static/api-specs/{crds => toolhive-crds}/virtualmcpservers.schema.json (100%) create mode 100644 static/img/icons/stacklok-website-icons-circuit-dark-green.svg create mode 100644 static/img/icons/stacklok-website-icons-circuit-light-green.svg rename static/img/{mcp-servers => icons}/stacklok-website-icons-efficiency-dark-green.svg (100%) rename static/img/{mcp-servers => icons}/stacklok-website-icons-efficiency-light-green.svg (100%) create mode 100644 static/img/icons/stacklok-website-icons-security-dark-green.svg create mode 100644 static/img/icons/stacklok-website-icons-security-light-green.svg diff --git a/.claude/agents/mcp-guide-writer.md b/.claude/agents/mcp-guide-writer.md index 60668d47..c02fa671 100644 --- a/.claude/agents/mcp-guide-writer.md +++ b/.claude/agents/mcp-guide-writer.md @@ -28,7 +28,7 @@ Your primary responsibilities: 3. **Ensure Technical Accuracy**: All configuration examples must be valid and tested. Reference the existing ToolHive documentation in the `docs/toolhive/` directory as the source of truth for: - Available `thv` CLI commands and their syntax (reference: `docs/toolhive/reference/cli/*.md` or run `thv --help`) - - Kubernetes CRD specifications and fields (reference: `static/api-specs/toolhive-crd-api.md`) + - Kubernetes CRD specifications and fields (reference: `static/api-specs/toolhive-crds/*.schema.json`) - UI configuration options and workflows (reference: `docs/toolhive/guides-ui/*`) 4. **Follow Documentation Standards**: Adhere to the project's writing style guide (`STYLE_GUIDE.md`) including: diff --git a/.claude/skills/upstream-release-docs/SKILL.md b/.claude/skills/upstream-release-docs/SKILL.md index 918c1a1f..9f7da764 100644 --- a/.claude/skills/upstream-release-docs/SKILL.md +++ b/.claude/skills/upstream-release-docs/SKILL.md @@ -263,17 +263,18 @@ Apply the approved changes: 6. **CRD reference updates**: the Kubernetes CRD reference is partially auto-generated. If the release touches CRDs, know the split: - **Fully auto-generated** (do not hand-edit): - - `static/api-specs/crds/*.schema.json` - extracted JSON Schema per CRD - - `static/api-specs/crds/*.example.yaml` - minimal required-fields YAML example - - `static/api-specs/crds/index.json` - metadata + cross-reference graph - - `static/api-specs/crds/sidebar.json` - sidebar fragment consumed by `sidebars.ts` - - `src/components/CRDReference/schemas.ts` - Kind -> schema index imported by the `` component - - `docs/toolhive/reference/crds/*.mdx` - per-CRD pages (including the landing `index.mdx`) + **Fully auto-generated** (do not hand-edit): the exact paths depend on the project's `crds:` array in `.github/upstream-projects.yaml`. Each entry declares an `out` (static/ schema dir) and `pages` (docs/ MDX dir). For toolhive OSS these are `static/api-specs/toolhive-crds/` and `docs/toolhive/reference/crds/`; for enterprise or gateway CRDs they will differ. Within each pair, the generated files are: + - `/*.schema.json` - extracted JSON Schema per CRD + - `/*.example.yaml` - minimal required-fields YAML example + - `/index.json` - metadata + cross-reference graph + - `/sidebar.json` - sidebar fragment consumed by `sidebars.ts` + - `/*.mdx` - per-CRD pages (including the landing `index.mdx`) - These come from `scripts/extract-crd-schemas.mjs` + `scripts/generate-crd-pages.mjs`, run by `scripts/update-toolhive-reference.sh`. Regenerating just means re-running the script; do not edit the MDX directly. + Plus one repo-wide generated file spanning all sets: `src/components/CRDReference/all-schemas.ts`, the consolidated Kind -> schema barrel consumed by `` (emitted by `scripts/generate-crd-barrel.mjs`). Do not hand-edit it. - **Hand-written overrides** (`scripts/lib/crd-intros.mjs`): every CRD in `index.json` publishes automatically using schema-derived defaults. Entries in this file override those defaults to polish a page. All fields are optional: + These come from `scripts/extract-crd-schemas.mjs` + `scripts/generate-crd-pages.mjs` + `scripts/generate-crd-barrel.mjs`, all driven by `scripts/upstream-release/extract-crds.mjs`. Regenerating means re-running: `node scripts/upstream-release/extract-crds.mjs --id `. Do not edit the MDX or schema files directly. + + **Hand-written overrides** (`scripts/lib/crd-intros.mjs`): every CRD in any project's `index.json` publishes automatically using schema-derived defaults. Entries in this file override those defaults to polish a page. All fields are optional: - `slug`: URL segment and MDX filename. Default: `Kind.toLowerCase()`. - `group`: `'core'` or `'shared'`. Default: `'shared'`. - `summary`: one-sentence DocCard pitch. Default: first sentence of the cleaned upstream schema description. @@ -282,7 +283,7 @@ Apply the approved changes: **When the release adds a new CRD**: - The release PR auto-publishes the new CRD with schema-derived defaults and flags it in a `[!NOTE]` block. No blocker. - - Review the generated page. If the upstream kubebuilder description is thin or the CRD should live in the `core` group or appear higher on the landing page, add an override entry for that Kind to `crd-intros.mjs` and re-run `node scripts/generate-crd-pages.mjs`. Overridden entries render before defaults-only entries within each group, in the order they are declared in the file. + - Review the generated page. If the upstream kubebuilder description is thin or the CRD should live in the `core` group or appear higher on the landing page, add an override entry for that Kind to `crd-intros.mjs` and re-run `node scripts/upstream-release/extract-crds.mjs --id `. Overridden entries render before defaults-only entries within each group, in the order they are declared in the file. - Commit the intros change plus the regenerated outputs. You can also land this as a follow-up PR after the release PR merges. **When the release modifies an existing CRD**: the schema/example regenerate automatically. If the CRD has no override entry, the intro prose will track the upstream description automatically. If it does have an override entry, update the `intro` only if the CRD's role materially shifted. diff --git a/.github/upstream-projects.yaml b/.github/upstream-projects.yaml index bfbc00a4..11468899 100644 --- a/.github/upstream-projects.yaml +++ b/.github/upstream-projects.yaml @@ -7,11 +7,20 @@ # reacts to those PRs, shallow-clones the upstream, and adds # source-verified content edits via the upstream-release-docs skill. # -# For `stacklok/toolhive` specifically, most reference artifacts are -# declared as `assets:` below (synced by sync-assets.mjs) — plus one -# workflow-step in upstream-release-docs.yml that downloads the CRD -# manifests tarball and runs extract-crd-schemas.mjs + generate-crd- -# pages.mjs to produce our opinionated per-CRD MDX pages. +# For projects that ship Kubernetes CRDs, declare a `crds:` array +# alongside `assets:`. Each entry drives one run of extract-crd-schemas.mjs +# + generate-crd-pages.mjs via scripts/upstream-release/extract-crds.mjs. +# Source kinds: `release_asset:` (from the GitHub release tarball) or +# `source:` (directory inside the upstream clone). Required per-entry +# fields: `out` (static/ output dir), `pages` (docs/ MDX output dir). +# Optional: `landing_title`, `landing_description`, `landing_intro` to +# customize the generated landing page. +# This array is the single source of truth for the CRD sets: the remark +# plugin (docusaurus.config.ts), the sidebar fragments (sidebars.ts), and +# the consolidated schema barrel are all derived from it via +# scripts/lib/crd-sets.mjs, so adding a set is a YAML edit plus a regen. +# Multiple entries per project are supported for suites that ship more +# than one CRD set (e.g. enterprise platform + AI gateway). # # You can also edit `version:` by hand (to backfill or reset) and # dispatch the upstream-release-docs workflow manually in bootstrap @@ -66,6 +75,41 @@ projects: destination: static/api-specs/publisher-provided.schema.json - release_asset: skill.schema.json destination: static/api-specs/skill.schema.json + crds: + - release_asset: thv-crds.tar.gz + out: static/api-specs/toolhive-crds + pages: docs/toolhive/reference/crds + landing_title: Kubernetes CRD reference + landing_description: >- + Reference for all ToolHive Kubernetes Operator custom resource + definitions. + landing_intro: >- + The ToolHive operator manages MCP workloads using Kubernetes custom + resources. Each page below documents one resource - its fields, + defaults, validation rules, and a minimal example manifest - and + links to the other resources it references. + + - id: stacklok-enterprise-platform + repo: stacklok/stacklok-enterprise-platform + # version: intentionally omitted - no public release cadence yet. + # CRDs are sourced from the local clone via --clone in development + # and will use release_asset: once the platform ships releases. + docs_paths: + - docs/platform + crds: + - source: enterprise/toolhive-enterprise/helm/files/crds + out: static/api-specs/enterprise-crds + pages: docs/platform/reference/crds + landing_title: Platform CRD reference + landing_description: >- + Reference for Stacklok Enterprise authorization custom resource + definitions. + landing_intro: >- + The Stacklok Enterprise platform extends the Kubernetes API with + custom resources for role-based access control and authorization + policy enforcement. Each page below documents one resource - its + fields, defaults, validation rules, and a minimal example manifest + - and links to the other resources it references. - id: toolhive-studio repo: stacklok/toolhive-studio diff --git a/.github/workflows/upstream-release-docs.yml b/.github/workflows/upstream-release-docs.yml index 1168a5c7..191ccf5c 100644 --- a/.github/workflows/upstream-release-docs.yml +++ b/.github/workflows/upstream-release-docs.yml @@ -11,11 +11,12 @@ name: Upstream Release Docs # 3. Syncs declared upstream assets into static/ via sync-assets.mjs # (release-asset downloads, tarball extractions, file-from-clone # copies — see `assets:` in upstream-projects.yaml) -# 4. For toolhive specifically, downloads the CRD manifests tarball -# and runs extract-crd-schemas.mjs + generate-crd-pages.mjs + -# bundle-upstream-schema.mjs to produce our opinionated reference -# MDX (the CRD tarball and toolhive-core schemas are shipped as -# release assets by stacklok/toolhive#4982) +# 4. For any project that declares a `crds:` key in upstream-projects.yaml, +# runs extract-crds.mjs to drive extract-crd-schemas.mjs + +# generate-crd-pages.mjs for each declared CRD set. Each set writes +# to its own out/pages dirs so multiple CRD sources (OSS operator, +# enterprise platform, AI gateway, etc.) never clobber each other. +# bundle-upstream-schema.mjs also runs here for toolhive specifically. # 5. Runs two Claude Opus sessions: a generation pass running the # upstream-release-docs skill, then a fresh-context editorial # pass running docs-review over the changed files. Both have @@ -361,27 +362,28 @@ jobs: --repo "$REPO" \ --tag "$NEW_TAG" - # toolhive ships CRD manifests as a release asset tarball (see - # stacklok/toolhive#4982). We extract it to a temp dir and run our - # opinionated transforms: extract per-CRD JSON schemas + examples, - # generate MDX reference pages. Bundling the upstream-registry - # schema (resolves the remote MCP-server $refs for our JSON-Schema - # viewer) also runs here. - - name: Extract CRDs + generate reference MDX (toolhive) - if: steps.detect.outputs.id == 'toolhive' + # For any project with a `crds:` key in upstream-projects.yaml, run + # extract-crds.mjs to drive extract-crd-schemas.mjs + + # generate-crd-pages.mjs for each declared CRD set. The script is a + # no-op for projects with no crds: entries, so no `if:` guard is needed. + - name: Extract CRDs + generate reference MDX env: + PROJECT_ID: ${{ steps.detect.outputs.id }} + CLONE_DIR: ${{ steps.clone.outputs.scratch_dir }} REPO: ${{ steps.detect.outputs.repo }} NEW_TAG: ${{ steps.detect.outputs.new_tag }} run: | - TMP=$(mktemp -d) - gh release download "$NEW_TAG" --repo "$REPO" \ - --pattern "thv-crds.tar.gz" --dir "$TMP" - mkdir -p "$TMP/crds" - tar -xzf "$TMP/thv-crds.tar.gz" -C "$TMP/crds" - TOOLHIVE_CRD_DIR="$TMP/crds" node scripts/extract-crd-schemas.mjs - node scripts/generate-crd-pages.mjs - node scripts/bundle-upstream-schema.mjs - rm -rf "$TMP" + node scripts/upstream-release/extract-crds.mjs \ + --id "$PROJECT_ID" \ + --clone "$CLONE_DIR" \ + --repo "$REPO" \ + --tag "$NEW_TAG" + + # toolhive-specific: bundle the upstream-registry schema to resolve + # remote MCP-server $refs for the JSON-Schema viewer. + - name: Bundle upstream schema (toolhive) + if: steps.detect.outputs.id == 'toolhive' + run: node scripts/bundle-upstream-schema.mjs # Commit AND PUSH the refreshed reference assets (synced release- # asset files + regenerated toolhive CRD MDX if applicable) before @@ -438,6 +440,22 @@ jobs: ") echo "docs_paths=$HINTS" >> "$GITHUB_OUTPUT" + # Resolve the CRD pages dirs for this project so the autogen-detect + # step and skill prompts can reference them without hardcoding paths. + # Space-separated; empty string when the project declares no crds:. + - name: Resolve CRD pages dirs + id: crd_pages + env: + PROJECT_ID: ${{ steps.detect.outputs.id }} + run: | + DIRS=$(node -e " + const yaml = require('yaml'); + const fs = require('fs'); + const p = yaml.parse(fs.readFileSync('.github/upstream-projects.yaml','utf8')).projects.find(x=>x.id===process.env.PROJECT_ID); + console.log((p?.crds ?? []).map(c => c.pages).filter(Boolean).join(' ')); + " 2>/dev/null || true) + echo "dirs=$DIRS" >> "$GITHUB_OUTPUT" + # claude-code-action@v1 rejects track_progress: true on # workflow_dispatch events with "track_progress is only # supported for events: pull_request, issue_comment, ...". @@ -536,8 +554,8 @@ jobs: ${{ steps.clone.outputs.scratch_dir }} instead of `gh api contents?ref=` -- it's already at the tag and doesn't consume API quota. Do not hand-edit auto-generated - reference under docs/toolhive/reference/cli/, - docs/toolhive/reference/crds/, or static/api-specs/ -- + reference under static/api-specs/, docs/toolhive/reference/cli/, + or any CRD pages dirs (${{ steps.crd_pages.outputs.dirs }}) -- earlier workflow steps sync those from release assets. Project dependencies are already installed by an earlier @@ -660,9 +678,9 @@ jobs: new claims; only refine what's already there. Do NOT re-run /upstream-release-docs. Do NOT touch files - under docs/toolhive/reference/cli/, static/api-specs/, - or docs/toolhive/reference/crds/ — those are - regenerated from release assets and aren't yours to edit. + under static/api-specs/, docs/toolhive/reference/cli/, + or any CRD pages dirs (${{ steps.crd_pages.outputs.dirs }}) -- + those are regenerated from release assets and aren't yours to edit. Do NOT touch GAPS.md, NO_CHANGES.md, or SUMMARY.md at the repo root if they exist -- they're signal files @@ -866,12 +884,20 @@ jobs: # the staged diff represents skill-introduced changes only. # The Augment step consumes `touched` directly and composes # the CAUTION alert itself -- no note block assembled here. + env: + PROJECT_ID: ${{ steps.detect.outputs.id }} + CRD_PAGES_DIRS: ${{ steps.crd_pages.outputs.dirs }} run: | git add -A - TOUCHED=$(git diff --cached --name-only -- \ - 'docs/toolhive/reference/cli/' \ - 'static/api-specs/' \ - 'docs/toolhive/reference/crds/' | paste -sd, - || true) + # Always watch: CLI reference (toolhive only) and all static/api-specs/. + # Plus any CRD pages dirs declared in the project's crds: config. + DIFF_ARGS=() + [ "$PROJECT_ID" = "toolhive" ] && DIFF_ARGS+=('docs/toolhive/reference/cli/') + DIFF_ARGS+=('static/api-specs/') + for dir in $CRD_PAGES_DIRS; do + DIFF_ARGS+=("${dir}/") + done + TOUCHED=$(git diff --cached --name-only -- "${DIFF_ARGS[@]}" | paste -sd, - || true) echo "touched=$TOUCHED" >> "$GITHUB_OUTPUT" - name: Commit and push @@ -997,6 +1023,7 @@ jobs: GAPS_BLOCK: ${{ steps.signals.outputs.gaps_block }} SUMMARY_BLOCK: ${{ steps.signals.outputs.summary_block }} AUTOGEN_TOUCHED: ${{ steps.autogen.outputs.touched }} + CRD_PAGES_DIRS: ${{ steps.crd_pages.outputs.dirs }} COMPARE_OK: ${{ steps.reviewers.outputs.compare_ok }} MENTION_BLOCK: ${{ steps.reviewers.outputs.mention_block }} ASSIGN_LIST: ${{ steps.reviewers.outputs.list }} @@ -1235,9 +1262,12 @@ jobs: echo "auto-fixes are applied after." echo "" echo "Auto-synced paths — do not hand-edit these in review:" - echo "- \`docs/toolhive/reference/cli/\`" - echo "- \`docs/toolhive/reference/crds/\`" echo "- \`static/api-specs/\`" + echo "- \`docs/toolhive/reference/cli/\` (toolhive only)" + # Emit one bullet per CRD pages dir declared in the project config. + for dir in $CRD_PAGES_DIRS; do + echo "- \`${dir}/\`" + done echo "" echo "If a \"Gaps needing human context\" section is present above," echo "each entry includes a paste-ready **Helper prompt for local" diff --git a/AGENTS.md b/AGENTS.md index c8d805e2..b62ca3be 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -231,6 +231,11 @@ ALWAYS use these exact terms and capitalizations. When editing documentation, re - large language model (LLM) - Visual Studio Code ("VS Code" after first use) - Virtual MCP Server (vMCP) - a feature of ToolHive that aggregates multiple MCP servers into a single endpoint; use "Virtual MCP Server (vMCP)" on first use, "vMCP" thereafter +- Stacklok Enterprise - the commercial, enterprise-licensed distribution of ToolHive, adding turnkey IdP integration, centralized policy enforcement, hardened and signed releases, and SLA-backed support +- Stacklok Desktop - the enterprise edition of the ToolHive desktop app, with enterprise lockdown policies controlled by the Enterprise Manager (not "Enterprise UI", "Enterprise Studio", or "Stacklok UI") +- Enterprise Manager - the Stacklok Enterprise component that pushes policy configuration to Stacklok Desktop and the Stacklok CLI +- Enterprise Cloud UI - the Stacklok Enterprise web console for browsing and managing the MCP server catalog +- enterprise authorization - the Stacklok Enterprise capability that compiles RBAC-style custom resources (roles, bindings, and per-server attachment policies) into the Cedar policies ToolHive enforces. It is a feature, not a named component: write it lowercase, capitalizing only at the start of a sentence, heading, or title. Don't treat it as a proper noun like the Enterprise Manager. If you encounter a term not listed here that appears frequently in the documentation, consider adding it to this list for consistency. @@ -299,7 +304,7 @@ Stacklok Enterprise includes turnkey integrations for common identity providers. ```ts title="sidebars.ts" { type: 'doc', - id: 'toolhive/guides-enterprise/config-server', + id: 'toolhive/enterprise-manager/config-server', className: 'enterprise-only', } ``` diff --git a/STYLE-GUIDE.md b/STYLE-GUIDE.md index 39ed84ba..08d1c18a 100644 --- a/STYLE-GUIDE.md +++ b/STYLE-GUIDE.md @@ -323,16 +323,18 @@ Specific guidelines for Docusaurus: These are the projects we work on, and a short description of each one. -**Stacklok MCP Platform**: Stacklok’s commercial offering for enterprises. It -consists of the ToolHive platform, commercial support terms, services hours from -the Stacklok Applied AI engineering team, and access to a collection of hardened -MCP server images. +**Stacklok Enterprise**: Stacklok’s commercial offering for enterprises: the +enterprise-licensed distribution of ToolHive. It adds turnkey identity provider +integration, centralized policy enforcement, hardened and signed releases, and +SLA-backed support, plus commercial support terms, services hours from the +Stacklok Applied AI engineering team, and access to a collection of hardened MCP +server images. **ToolHive**: A collection of open source projects that form the foundation of -the Stacklok MCP Platform. ToolHive includes everything you need to use MCP -servers in production. It’s made up of four key components: the Runtime, -Registry Server, Gateway, and Portal. It's written bi-capitalized as one word -(not "Toolhive" or "Tool Hive"). +Stacklok Enterprise. ToolHive includes everything you need to use MCP servers in +production. It’s made up of four key components: the Runtime, Registry Server, +Gateway, and Portal. It's written bi-capitalized as one word (not "Toolhive" or +"Tool Hive"). **ToolHive Runtime**: The core ToolHive CLI, desktop UI app, and Kubernetes Operator. ToolHive manages containerized MCP servers on your local machine, in @@ -354,6 +356,18 @@ thereafter. **ToolHive Portal**: A web-based frontend to the ToolHive Registry Server. +**Stacklok Desktop**: The enterprise edition of the ToolHive desktop app +(ToolHive UI). It adds OIDC sign-in and enterprise lockdown policies controlled +by the Enterprise Manager. Not "Enterprise UI", "Enterprise Studio", or +"Stacklok UI". + +**Enterprise Manager**: The Stacklok Enterprise component that pushes policy +configuration to Stacklok Desktop and the Stacklok CLI, controlling client +behavior across the organization. + +**Enterprise Cloud UI**: The Stacklok Enterprise web console for browsing and +managing the MCP server catalog. + ## Word list & glossary Common terms used in Stacklok content: @@ -364,9 +378,8 @@ sentence. **OSS**: Abbreviation for "open source software". -**Stacklok**: The company behind ToolHive and the Stacklok Enterprise MCP -Platform. It's written as one word with a single capital (not "StackLok" or -"Stacklock"). +**Stacklok**: The company behind ToolHive and Stacklok Enterprise. It's written +as one word with a single capital (not "StackLok" or "Stacklock"). ### Products/brands diff --git a/docs/ai-gateway/index.mdx b/docs/ai-gateway/index.mdx new file mode 100644 index 00000000..a4ee8b25 --- /dev/null +++ b/docs/ai-gateway/index.mdx @@ -0,0 +1,41 @@ +--- +title: AI Gateway +description: + Govern, secure, and control access to large language model providers from one + self-hosted enterprise gateway. +--- + +The Stacklok AI Gateway is a self-hosted enterprise gateway that sits between +your AI tools and large language model (LLM) providers. Deployed in your +environment, every AI request hits your policy, identity, and audit controls +before it reaches a provider. It gives platform and security teams a single +control point for cost, compliance, and access to models like those from OpenAI, +Anthropic, AWS Bedrock, Azure OpenAI, and Google Vertex AI. + +The AI Gateway is part of Stacklok Enterprise. It complements ToolHive: ToolHive +governs the **tools** your agents can use, while the AI Gateway governs the +**models** they can call. + +## What you can do + +- Cap AI spend in real time with token and cost budgets per user, team, agent, + or org, reconciled to your finance team's reporting cycle +- Block PII, financial data, regulated identifiers, and your own custom patterns + at the gateway, with fail-safe denial when policy can't be enforced +- Capture every LLM request in an audit trail and stream it to your security + information and event management (SIEM) system +- Route logical model names across multiple providers with failover +- Tie every request to an identity from your IdP, with provider keys locked in + the gateway and access revoked on the next request when a user or agent is + offboarded +- Govern people and agents as first-class peers under the same policies, + budgets, and audit trail + +:::note[Documentation in progress] + +The AI Gateway is rolling out as part of Stacklok Enterprise, and its full +setup, configuration, and reference documentation is on the way. In the +meantime, see [Stacklok Enterprise](../platform/index.mdx) for the platform it +is part of, or reach out to your Stacklok contact for early access. + +::: diff --git a/docs/platform/enterprise-authz/index.mdx b/docs/platform/enterprise-authz/index.mdx new file mode 100644 index 00000000..5e5d4f2d --- /dev/null +++ b/docs/platform/enterprise-authz/index.mdx @@ -0,0 +1,32 @@ +--- +title: Enterprise authorization +description: + Express MCP access control in familiar terms without learning a new policy + language. Namespace self-service for the teams that own the servers. +--- + +import DocCardList from '@theme/DocCardList'; + +:::enterprise + +Enterprise authorization lets cluster admins express MCP access control in +familiar terms, without learning a domain-specific policy language. + +Namespace owners can grant access to MCP servers they own, so the platform team +isn't in the loop on every change. + +[Learn more about Stacklok Enterprise](../index.mdx). + +::: + +## Where to start + + + +## CRD reference + +For the full field reference of each resource, see +[ClusterPlatformRole](../reference/crds/clusterplatformrole.mdx), +[ClusterPlatformRoleBinding](../reference/crds/clusterplatformrolebinding.mdx), +[PlatformRoleBinding](../reference/crds/platformrolebinding.mdx), and +[ToolhiveAuthorizationPolicy](../reference/crds/toolhiveauthorizationpolicy.mdx). diff --git a/docs/platform/enterprise-authz/intro.mdx b/docs/platform/enterprise-authz/intro.mdx new file mode 100644 index 00000000..a570b9c5 --- /dev/null +++ b/docs/platform/enterprise-authz/intro.mdx @@ -0,0 +1,137 @@ +--- +title: Introduction to enterprise authorization +description: + Express MCP access in RBAC terms and let the operator compile it to Cedar, so + role-based authorization scales across an MCP fleet. +--- + +:::enterprise + +Enterprise authorization is a capability of Stacklok Enterprise. For a full +comparison of ToolHive Community and Stacklok Enterprise capabilities, see +[Stacklok Enterprise](../index.mdx). + +::: + +At small scale, hand-writing a Cedar policy for each MCP server works fine. At +fleet scale, with many servers, many teams, and an identity-provider-driven +identity model, that approach falls apart. Enterprise authorization turns access +control into a set of declarative custom resources that the operator compiles +into the Cedar policies ToolHive enforces at runtime. + +## The problem + +Open source ToolHive supports rich Cedar policies on every MCP server, and that +flexibility is exactly what you want when you are securing one or two servers. +Once you are running a fleet, the same flexibility becomes a liability. Each +server carries its own policy file. The policies drift. There is no clean place +to express something as simple as "every engineer can read every server." + +The model also conflates two things that should be separate. A Cedar policy +defines _what_ a role can do _and_ _who_ holds that role, all in one document. +That works for a single team that owns a single server. It does not work when a +platform team needs to define a reusable role and a namespace owner needs to +grant that role to their own users without rewriting the platform team's policy. + +The CRDs introduced by enterprise authorization separate these concerns. Cluster +admins define roles once. Cluster admins or namespace owners bind those roles to +identity-provider groups and roles. A separate attachment object decides which +MCP servers a binding applies to. The operator compiles the combination into +Cedar. + +## The model + +Enterprise authorization is built on a small set of custom resources. If you +have used Kubernetes RBAC, the shape will feel familiar. + +- **`ClusterPlatformRole`** + ([reference](../reference/crds/clusterplatformrole.mdx)) defines _what_ a role + can do. It is product-agnostic. Per-product action vocabularies live under + `spec.productActions[]`, keyed by API group. The only supported product API + group is `toolhive.enterprise.stacklok.com`. Wildcard `"*"` is allowed, which + is how the built-in `writer` role grants every action. +- **`ClusterPlatformRoleBinding`** + ([reference](../reference/crds/clusterplatformrolebinding.mdx)) defines _who_ + gets a role, cluster-wide. It maps identity-provider groups and roles (as + carried in the JWT) to a `ClusterPlatformRole`. +- **`PlatformRoleBinding`** + ([reference](../reference/crds/platformrolebinding.mdx)) is the namespaced + sibling of `ClusterPlatformRoleBinding`. It lets a namespace owner author + grants for resources in their own namespace without involving the cluster + admin. Cluster-scoped and namespace-scoped bindings combine with union + semantics: a subject is granted a role if any matching binding says so. +- **`ToolhiveAuthorizationPolicy`** + ([reference](../reference/crds/toolhiveauthorizationpolicy.mdx)) defines + _where_ access applies. It attaches one or more role bindings to a specific + MCP target: an `MCPServer` or `MCPRemoteProxy`. It also supports optional + `ruleRestrictions` (narrow the bound role to a specific tool, prompt, or + resource), a `toolHintFilter` (gate by the `readOnlyHint` or `destructiveHint` + annotations the server advertises), and a `deny[]` list that compiles to a + Cedar `forbid` rule and overrides every grant. Multiple + `ToolhiveAuthorizationPolicy` objects can attach to the same target; their + effective principal sets and deny lists are unioned. + +## Claim mapping + +Bindings refer to identity-provider groups and roles. Those values come from +claims in the JWT that ToolHive validates on every request. By default, the +platform derives the claim names from the IdP type it detects, so a cluster +admin typically does not need to configure them. For Microsoft Entra ID, the +defaults are the `roles` claim (the app-role claim) and the `groups` claim. + +To override the defaults, set the `groupsClaim` and `rolesClaim` keys on the +platform identity-provider ConfigMap. The enterprise installer creates this +ConfigMap with a release-specific name in the platform namespace, for example +`stacklok-enterprise-platform-enterprise-manager-idp-config`. Find it with +`kubectl get configmap -A | grep idp-config`, then edit the keys to match the +claims your IdP emits. For the identity-provider side of this setup, where you +configure the IdP to emit those group and role claims, see +[Configure platform identity](../enterprise-platform/configure-identity.mdx). + +Inside each binding, the `from[]` field is a list of `PrincipalCondition` +objects. Each condition has two arrays: `groups[]` and `roles[]`. The two arrays +inside one condition are AND-ed: the subject must satisfy both the groups list +and the roles list to match. Across multiple conditions in the same `from[]`, +the conditions are OR-ed: a subject matches the binding if any one of them +matches. At least one of `groups[]` or `roles[]` must be non-empty in every +condition, which the CRD enforces with a CEL validation rule. + +For example, this binding matches users who are in the `mcp-engineers` group and +hold the `mcp-viewer` role, or anyone who holds the `mcp-admin` role: + +```yaml +from: + - groups: [mcp-engineers] + roles: [mcp-viewer] + - roles: [mcp-admin] +``` + +For the broader picture of how ToolHive verifies tokens and integrates with an +OIDC provider, see +[Authentication and authorization](../../toolhive/concepts/auth-framework.mdx). + +## Built-in roles + +The operator ships two `ClusterPlatformRole` objects out of the box. + +- `reader` grants the MCP read operations and `call_tool`. The `call_tool` + surface can be narrowed at bind time with `toolHintFilter.readOnlyHint: true`, + which restricts the binding to tools the MCP server itself annotates as + read-only. +- `writer` grants every action via wildcard. + +You can list them with `kubectl get clusterplatformroles` (or the short form, +`kubectl get cpr`). Build a custom `ClusterPlatformRole` only when you need a +narrower action vocabulary than `reader` and `writer` offer. + +## Next steps + +- [Quickstart - GitHub MCP with Entra ID](./quickstart-entra.mdx) for a hands-on + walkthrough against a real identity provider +- [Namespace self-service authorization](./namespace-self-service.mdx) to see + how a namespace owner authors their own bindings +- [ToolhiveAuthorizationPolicy](../reference/crds/toolhiveauthorizationpolicy.mdx) + for the full schema of the attachment object that ties everything together +- [Cedar policies](../../toolhive/concepts/cedar-policies.mdx) and the + [authorization policy reference](../../toolhive/reference/authz-policy-reference.mdx) + for the underlying policy language and the full vocabulary the operator emits diff --git a/docs/platform/enterprise-authz/namespace-self-service.mdx b/docs/platform/enterprise-authz/namespace-self-service.mdx new file mode 100644 index 00000000..c9b53021 --- /dev/null +++ b/docs/platform/enterprise-authz/namespace-self-service.mdx @@ -0,0 +1,168 @@ +--- +title: Namespace self-service authorization +description: + Hand authorization authoring to the team that owns the MCPServer, using + PlatformRoleBinding inside their own namespace. +--- + +This guide is for a team that owns an MCPServer in its own namespace and wants +to author authorization grants without involving the cluster admin. The cluster +admin still owns the role catalog; the team owns the bindings inside its +namespace. + +## When to use it + +Use namespace self-service when the team owns the workload, has an IdP group of +its own, and wants access changes to land in the same manifests as the MCPServer +itself. The cluster admin maintains a `ClusterPlatformRole` catalog; teams pick +from it rather than minting new verb sets. + +## How the trust model works + +The effective principal set on a `ToolhiveAuthorizationPolicy` is the union of: + +- every matching `ClusterPlatformRoleBinding` at the cluster scope, and +- every matching `PlatformRoleBinding` in the policy's own namespace. + +Namespace bindings can only add principals to that union. They cannot remove a +grant that a `ClusterPlatformRoleBinding` made cluster-wide. To deny an access +path for a specific MCP target, use the policy's `deny[]` list, which compiles +to a Cedar `forbid` rule and overrides every grant. The escalation direction is +intentional: the cluster admin retains a clean override path through +`ClusterPlatformRoleBinding`, and the team gets fast iteration on additive +grants. + +| Source | Scope | Who authors | +| ----------------------------- | -------------- | --------------- | +| `ClusterPlatformRoleBinding` | Cluster-wide | Cluster admin | +| `PlatformRoleBinding` | One namespace | Namespace owner | +| `ToolhiveAuthorizationPolicy` | One MCP target | Namespace owner | + +For full field details on each resource, see the +[ClusterPlatformRoleBinding](../reference/crds/clusterplatformrolebinding.mdx), +[PlatformRoleBinding](../reference/crds/platformrolebinding.mdx), and +[ToolhiveAuthorizationPolicy](../reference/crds/toolhiveauthorizationpolicy.mdx) +CRD references. + +## Walkthrough + +This walkthrough builds on the +[Quickstart: GitHub MCP with Entra ID](./quickstart-entra.mdx). The difference +is the namespace: the team owns `team-alpha` and has deployed an MCPServer named +`alpha-tools-mcp` there. The cluster admin has previously authored a `read-only` +`ClusterPlatformRole` and granted the team's namespace RBAC to manage +`platformrolebindings` and `toolhiveauthorizationpolicies` in `team-alpha`. + +### 1. Bind the IdP role to the catalog role + +Create a `PlatformRoleBinding` in `team-alpha` that maps the Entra app role +`mcp-team-alpha-engineers` to the cluster-published `read-only` +`ClusterPlatformRole`: + +```yaml title="alpha-engineers-readonly.yaml" +apiVersion: platform.enterprise.stacklok.com/v1alpha1 +kind: PlatformRoleBinding +metadata: + name: alpha-engineers-readonly + namespace: team-alpha +spec: + bindings: + - roleRef: + kind: ClusterPlatformRole + name: read-only + from: + - roles: + - mcp-team-alpha-engineers +``` + +```bash +kubectl apply -f alpha-engineers-readonly.yaml +``` + +This binding only contributes principals to authorization policies in +`team-alpha`. The `roleRef` points at a cluster-scoped role, so the team +consumes the catalog without authoring verbs. + +### 2. Attach the role to the MCP target + +Create a `ToolhiveAuthorizationPolicy` in `team-alpha` that binds the +`read-only` role to `alpha-tools-mcp`: + +```yaml title="alpha-tools-readonly.yaml" +apiVersion: toolhive.enterprise.stacklok.com/v1alpha1 +kind: ToolhiveAuthorizationPolicy +metadata: + name: alpha-tools-readonly + namespace: team-alpha +spec: + targetRef: + name: alpha-tools-mcp + bindings: + - roleRef: + kind: ClusterPlatformRole + name: read-only +``` + +```bash +kubectl apply -f alpha-tools-readonly.yaml +``` + +`targetRef.kind` defaults to `MCPServer`, so it's omitted. The policy's +namespace and the MCPServer's namespace must match. + +### 3. Wait for the policy to compile + +```bash +kubectl wait --for=condition=Compiled \ + tap/alpha-tools-readonly -n team-alpha --timeout=60s +``` + +When the `Compiled` condition flips to `True`, the operator has turned the +policy plus the matching `PlatformRoleBinding` into the Cedar policy bundle the +proxy will enforce. + +### 4. Verify with a token from the team's IdP group + +Obtain a token for a user assigned to `mcp-team-alpha-engineers` and call a tool +through the proxy at `alpha-tools-mcp`, following the pattern in +[step 6 of the Entra quickstart](./quickstart-entra.mdx#step-6-verify-the-policy-is-enforced). + +Expected outcome: tool calls that carry the `readOnlyHint` annotation return +`200`, and tool calls that don't carry it return `403`. The read-only role's +principal set was contributed entirely by the team's `PlatformRoleBinding`; no +cluster-wide binding had to change. + +## What namespace owners cannot do + +Self-service is bounded. A namespace owner cannot: + +- **Subtract a cluster-wide grant.** If a `ClusterPlatformRoleBinding` grants a + role to a principal that also matches a `ToolhiveAuthorizationPolicy` in the + team's namespace, the team cannot drop that principal locally. The options are + a `deny[]` entry on the authorization policy to override grants for one + specific MCP target, or a request to the cluster admin to change the + `ClusterPlatformRoleBinding`. +- **Author or edit a `ClusterPlatformRole`.** Roles live in the + cluster-admin-owned catalog. The team consumes them by `roleRef`. +- **Bind across namespaces.** A `PlatformRoleBinding` in `team-alpha` only + contributes principals to authorization policies whose namespace is + `team-alpha`. Cross-namespace effects require a `ClusterPlatformRoleBinding`, + which only the cluster admin can author. + +## Guard rails the cluster admin can enable + +The access gate on namespace self-service is namespace-level Kubernetes RBAC on +`platformrolebindings` and `toolhiveauthorizationpolicies`. A team that holds +those verbs can reference any `ClusterPlatformRole` in the cluster, including +high-power roles published for other teams. Treat the `ClusterPlatformRole` +catalog as a curated allow-list, and publish fewer, narrower roles when in +doubt. + +## Next steps + +- See every field on the namespace binding in the + [PlatformRoleBinding CRD reference](../reference/crds/platformrolebinding.mdx). +- See the cluster-scoped counterpart in the + [ClusterPlatformRoleBinding CRD reference](../reference/crds/clusterplatformrolebinding.mdx). +- Read how identity, roles, and bindings fit together in the + [Authentication and authorization framework](../../toolhive/concepts/auth-framework.mdx). diff --git a/docs/platform/enterprise-authz/quickstart-entra.mdx b/docs/platform/enterprise-authz/quickstart-entra.mdx new file mode 100644 index 00000000..3488f0c1 --- /dev/null +++ b/docs/platform/enterprise-authz/quickstart-entra.mdx @@ -0,0 +1,541 @@ +--- +title: Quickstart - GitHub MCP with Entra ID +description: + Pair Microsoft Entra ID with the GitHub MCP server and enforce role-based + authorization with a compiled ToolhiveAuthorizationPolicy. +--- + +By the end of this walkthrough, you'll stand up an Entra ID-authenticated GitHub +MCP server, attach a policy that gives one user full write access and another +user read-only access, and verify the split with live tool calls. The reader is +restricted by a tool annotation filter so the same `reader` +`ClusterPlatformRole` can be reused elsewhere with different scoping. An +optional final step demonstrates a second pattern: narrowing a broad role to a +named allow-list of tools at bind time. + +## What you'll learn + +- How to register an app and define app roles in Microsoft Entra ID +- How to deploy an Entra ID-authenticated GitHub MCP server with per-server OIDC +- How to map Entra app roles to built-in ToolHive platform roles +- How to attach a per-server policy that gives one user write access and another + read-only access +- How to verify the access split with live MCP tool calls +- How to narrow a role to a named allow-list of tools at bind time (optional) + +## Prerequisites + +Before starting, make sure you have: + +- A Kubernetes cluster running the Stacklok Enterprise distribution. See the + [Kubernetes quickstart](../../toolhive/guides-k8s/quickstart.mdx) for cluster + setup, then follow the enterprise installer instructions. +- `kubectl` configured against that cluster. +- A Microsoft Entra ID tenant where you have permission to create an app + registration, define app roles, and assign users. +- A + [GitHub personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) + (PAT) with `repo` scope. The policy gates what each user can do, so the PAT is + intentionally privileged. +- An MCP client that can send a bearer token. This guide uses the + [MCP Inspector](https://github.com/modelcontextprotocol/inspector) CLI, which + needs Node.js. + +## Step 1: Set up the Entra app registration + +In the Entra ID portal, create an app registration and configure it as follows: + +1. **App registrations** > **New registration**. Name it `toolhive-authz-demo` + with single-tenant account type. +2. **Authentication**. Configure the platform and flow you'll use to obtain user + access tokens in step 6. The setup is environment-specific: for a + public-client flow, for example, add **Mobile and desktop applications**, set + a redirect URI, and set **Allow public client flows** to **Yes**. +3. **Expose an API**. Accept the default Application ID URI of + `api://` and add a scope named `access_as_user`. +4. **App roles**. Create three roles with member type **Users/Groups**. Use + these exact **Value** strings - later manifests refer to them: + + | Display name | Value | + | ----------------- | -------------- | + | MCP admin | `mcp-admin` | + | MCP viewer | `mcp-viewer` | + | MCP code reviewer | `mcp-reviewer` | + +5. Open the matching **Enterprise application** > **Properties** and set + **Assignment required** to **Yes**, so unassigned users don't get tokens with + empty `roles` claims. +6. **Enterprise application** > **Users and groups** > **Add user/group**. + Assign one test user per role so each user receives exactly one of + `mcp-admin`, `mcp-viewer`, or `mcp-reviewer`. +7. Note the tenant ID and application (client) ID from the app's **Overview** + page for step 3. + +## Step 2: Create the namespace and GitHub PAT secret + +Create a namespace to hold the demo resources and a `Secret` that the MCP server +will mount as the GitHub PAT. + +```yaml title="00-namespace.yaml" +apiVersion: v1 +kind: Namespace +metadata: + name: authz-demo +``` + +```yaml title="00-secret-github-pat.yaml" +apiVersion: v1 +kind: Secret +metadata: + name: github-pat + namespace: authz-demo +type: Opaque +stringData: + token: '' +``` + +Replace `` with your classic PAT, then apply both files: + +```bash +kubectl apply -f 00-namespace.yaml -f 00-secret-github-pat.yaml +``` + +The MCPServer in the next step references this `Secret` by name through its +`secrets[]` field. For the other ways ToolHive can source secrets in Kubernetes, +see +[Run a server with secrets](../../toolhive/guides-k8s/run-mcp-k8s.mdx#run-a-server-with-secrets). + +## Step 3: Configure OIDC and deploy the GitHub MCP server + +Define an `MCPOIDCConfig` that points at your Entra tenant, then deploy the +GitHub MCP server with a reference to that config. This `MCPOIDCConfig` is +per-server: it tells the proxy in front of this MCP server how to validate +incoming tokens. It is independent of the +[platform-component identity](../enterprise-platform/configure-identity.mdx) +that the enterprise installer configures for the manager, UI, and Registry +Server, so you don't need that setup in place to follow this quickstart. Both +draw their group and role claims from the same identity provider. + +```yaml title="01-oidc-config.yaml" +apiVersion: toolhive.stacklok.dev/v1beta1 +kind: MCPOIDCConfig +metadata: + name: demo-entra-oidc + namespace: authz-demo +spec: + type: inline + inline: + issuer: 'https://login.microsoftonline.com//v2.0' +``` + +```yaml title="02-mcpserver-github.yaml" +apiVersion: toolhive.stacklok.dev/v1beta1 +kind: MCPServer +metadata: + name: github-demo + namespace: authz-demo +spec: + image: ghcr.io/github/github-mcp-server:v1.0.3 + transport: stdio + proxyPort: 8080 + oidcConfigRef: + name: demo-entra-oidc + audience: '' + secrets: + - name: github-pat + key: token + targetEnvName: GITHUB_PERSONAL_ACCESS_TOKEN + env: + - name: GITHUB_API_URL + value: https://api.github.com + resources: + limits: + cpu: '200m' + memory: '256Mi' + requests: + cpu: '100m' + memory: '128Mi' +``` + +Replace `` with your directory (tenant) ID and `` with the +application (client) ID from step 1, then apply both manifests and wait for the +server to come up: + +```bash +kubectl apply -f 01-oidc-config.yaml -f 02-mcpserver-github.yaml +kubectl -n authz-demo wait --for=condition=Ready \ + mcpserver/github-demo --timeout=2m +``` + +:::warning[Match the audience to what Entra actually emits] + +`oidcConfigRef.audience` must match the `aud` claim Entra puts in your access +tokens. The manifest above uses the client GUID, which is the v2 default. If +your tenant is configured to emit the Application ID URI (`api://`) +instead, set `audience` to that string. If you hit `401` errors later, step 9 +covers how to inspect a token and fix the mismatch. + +::: + +:::note[Production considerations] + +This quickstart trades production hardening for a short setup path. Two choices +are worth revisiting before you run this pattern in production: + +- **The shared PAT hides the calling user from the backend.** ToolHive + authenticates and authorizes each user, but every GitHub API call still + reaches GitHub as the single bot identity behind the PAT. If you need GitHub + to see who made each call (for its own audit log or fine-grained access), use + a per-user OAuth flow to the backend instead of a shared token. See + [token exchange](../../toolhive/guides-k8s/token-exchange-k8s.mdx) for + propagating user identity to upstream services. +- **`transport: stdio` runs one process per session.** It keeps the manifest + simple but doesn't scale to a fleet. The GitHub MCP server also offers a + `streamable-http` mode, which serves many sessions from one deployment and + uses OAuth rather than a PAT. See the + [github-mcp-server streamable-http docs](https://github.com/github/github-mcp-server/blob/main/docs/streamable-http.md). + +::: + +## Step 4: Apply the role bindings (admin and viewer) + +Map the two Entra app roles to the built-in `writer` and `reader` +`ClusterPlatformRole` objects. This is the only place IdP role names appear in +the manifests, every later step refers to the platform roles by name. + +```yaml title="10-trb-admin-viewer.yaml" +apiVersion: platform.enterprise.stacklok.com/v1alpha1 +kind: ClusterPlatformRoleBinding +metadata: + name: demo-admin-viewer-binding +spec: + bindings: + - roleRef: + kind: ClusterPlatformRole + name: writer + from: + - roles: ['mcp-admin'] + - roleRef: + kind: ClusterPlatformRole + name: reader + from: + - roles: ['mcp-viewer'] +``` + +```bash +kubectl apply -f 10-trb-admin-viewer.yaml +``` + +This `ClusterPlatformRoleBinding` says: any caller whose JWT carries +`roles: ["mcp-admin"]` gets the `writer` role, and any caller with +`roles: ["mcp-viewer"]` gets the `reader` role. The binding is cluster-scoped +and doesn't yet apply to any server; that's the job of the next step. + +## Step 5: Apply the per-server policy with reader and writer + +The `ToolhiveAuthorizationPolicy` attaches roles to a specific MCP server. This +policy binds the `writer` role unrestricted, and binds the `reader` role with a +`toolHintFilter` that limits the binding to tools that declare the MCP +`readOnlyHint` annotation. + +```yaml title="11-tap-reader-writer.yaml" +apiVersion: toolhive.enterprise.stacklok.com/v1alpha1 +kind: ToolhiveAuthorizationPolicy +metadata: + name: github-demo-rw-policy + namespace: authz-demo +spec: + targetRef: + name: github-demo + bindings: + - roleRef: + kind: ClusterPlatformRole + name: writer + - roleRef: + kind: ClusterPlatformRole + name: reader + toolHintFilter: + readOnlyHint: true +``` + +`targetRef.kind` defaults to `MCPServer`, so it's omitted. `toolHintFilter` +lives on the binding rather than on the role itself: the same `reader` role can +be reused in a different policy without a filter, or with a different filter +like `destructiveHint: false`. The role describes the verbs; the binding decides +which tools at the target satisfy those verbs. + +Apply the policy and wait for the operator to compile it: + +```bash +kubectl apply -f 11-tap-reader-writer.yaml +kubectl wait --for=condition=Compiled tap/github-demo-rw-policy \ + -n authz-demo --timeout=60s +``` + +The `Compiled` condition flips to `True` once the operator has turned the policy +into the Cedar policy bundle the proxy will enforce. If it doesn't compile, see +[step 9](#step-9-troubleshooting). + +## Step 6: Verify the policy is enforced + +Point an MCP client at the server using each user's token, and call a read tool +(`list_issues`) and a write tool (`issue_write`) to see the policy in action. + +In a real deployment, clients reach the MCP server through the ingress or +gateway your platform exposes, and you point the client at that URL. For a quick +local check against this quickstart, forward the proxy Service to your machine. +This is a validation convenience, not a production access path: + +```bash +kubectl -n authz-demo port-forward svc/mcp-github-demo-proxy 18080:8080 +``` + +The server is then reachable at `http://localhost:18080/mcp`, which the commands +below use. + +### Get a token for each test user + +Obtain an access token for each test user from your identity provider: the user +assigned `mcp-admin` and the user assigned `mcp-viewer` (plus `mcp-reviewer` for +step 7). Use whichever OAuth 2.0 flow your environment supports - authorization +code with PKCE, device code, or a client your platform already uses. The policy +evaluates the token's contents, not how you acquired it, so each token must: + +- be an access token for this app, with an audience matching + `spec.oidcConfigRef.audience` on the MCPServer (the + `api:///access_as_user` scope produces it), and +- carry the user's app role in the `roles` claim, which requires the user to be + assigned to that role on the enterprise application (step 1). + +Decode a token with `jwt decode "$TOKEN"` to confirm its `aud` and `roles` +before continuing. Put each user's token in a shell variable - `TOKEN_ADMIN`, +`TOKEN_VIEWER`, and `TOKEN_REVIEWER` - for the calls below. + +### Verify with an MCP client + +Use any MCP client that can send a bearer token. This guide uses the +[MCP Inspector](https://github.com/modelcontextprotocol/inspector) in CLI mode, +which runs the MCP handshake (`initialize`, `notifications/initialized`, +`tools/list`) before your call. That handshake matters here: the proxy only +learns a tool's `readOnlyHint` annotation once `tools/list` has run on the +session, and the reader binding's `toolHintFilter` consults it. A standard +client does this for you. + +Call a read tool (`list_issues`) and a write tool (`issue_write`) with each +user's token, substituting your repository for `/`. As the admin, +both succeed: + +```bash +npx @modelcontextprotocol/inspector --cli http://localhost:18080/mcp \ + --transport http --header "Authorization: Bearer $TOKEN_ADMIN" \ + --method tools/call --tool-name list_issues \ + --tool-arg owner= --tool-arg repo= + +npx @modelcontextprotocol/inspector --cli http://localhost:18080/mcp \ + --transport http --header "Authorization: Bearer $TOKEN_ADMIN" \ + --method tools/call --tool-name issue_write \ + --tool-arg method=create --tool-arg owner= --tool-arg repo= \ + --tool-arg title="authz smoke test" +``` + +As the viewer, the read succeeds but the write is denied (same two commands with +`$TOKEN_VIEWER`): + +```bash +npx @modelcontextprotocol/inspector --cli http://localhost:18080/mcp \ + --transport http --header "Authorization: Bearer $TOKEN_VIEWER" \ + --method tools/call --tool-name list_issues \ + --tool-arg owner= --tool-arg repo= + +npx @modelcontextprotocol/inspector --cli http://localhost:18080/mcp \ + --transport http --header "Authorization: Bearer $TOKEN_VIEWER" \ + --method tools/call --tool-name issue_write \ + --tool-arg method=create --tool-arg owner= --tool-arg repo= \ + --tool-arg title="should be denied" +``` + +Both users can read. The admin's `issue_write` creates the issue and returns its +URL. The viewer's `issue_write` is rejected by the proxy before it reaches the +server: + +| User | `list_issues` | `issue_write` (create) | +| ------------ | ------------- | ------------------------ | +| `mcp-admin` | succeeds | succeeds (issue created) | +| `mcp-viewer` | succeeds | denied | + +The viewer's denial surfaces as a transport error carrying the proxy's `403`: + +```text +Failed to call tool issue_write: Streamable HTTP error: Error POSTing to +endpoint: {"Result":null,"Error":{"code":403,"message":"Unauthorized"},"ID":{}} +``` + +That 403 is the policy doing its job: `issue_write` does not carry the +`readOnlyHint` annotation, so the reader binding's `toolHintFilter` excludes it, +while the read-only `list_issues` is allowed. + +:::note[Tool names track the server version] + +This guide targets `github-mcp-server:v1.0.3`, where issue writes go through the +`issue_write` tool. Other versions may name tools differently. Run the client's +`tools/list` method to see what your server exposes. + +::: + +## Step 7: (Optional) Give a reviewer access to only the tools they need + +A code reviewer doesn't need every tool the GitHub MCP server exposes, just the +ones for reading pull requests, searching code, and listing issues. Grant them +exactly that set by listing tool names in `ruleRestrictions.tools` on the +binding. The underlying `code-reviewer` role can still be reused elsewhere with +a different tool list; the per-target policy decides the narrowing. + +```yaml title="20-platformrole-code-reviewer.yaml" +apiVersion: platform.enterprise.stacklok.com/v1alpha1 +kind: ClusterPlatformRole +metadata: + name: code-reviewer +spec: + description: 'Code-reviewer baseline. Scoped at binding time.' + productActions: + - apiGroup: toolhive.enterprise.stacklok.com + actions: + - list_tools + - call_tool + - list_prompts + - get_prompt + - list_resources + - read_resource +``` + +```yaml title="21-trb-reviewer.yaml" +apiVersion: platform.enterprise.stacklok.com/v1alpha1 +kind: ClusterPlatformRoleBinding +metadata: + name: demo-reviewer-binding +spec: + bindings: + - roleRef: + kind: ClusterPlatformRole + name: code-reviewer + from: + - roles: ['mcp-reviewer'] +``` + +```yaml title="22-tap-code-reviewer.yaml" +apiVersion: toolhive.enterprise.stacklok.com/v1alpha1 +kind: ToolhiveAuthorizationPolicy +metadata: + name: github-reviewer-policy + namespace: authz-demo +spec: + targetRef: + name: github-demo + bindings: + - roleRef: + kind: ClusterPlatformRole + name: code-reviewer + ruleRestrictions: + - tools: + - list_issues + - pull_request_read + - search_code +``` + +Apply the three files and wait for compilation: + +```bash +kubectl apply \ + -f 20-platformrole-code-reviewer.yaml \ + -f 21-trb-reviewer.yaml \ + -f 22-tap-code-reviewer.yaml +kubectl wait --for=condition=Compiled tap/github-reviewer-policy \ + -n authz-demo --timeout=60s +``` + +Fetch a token for the `mcp-reviewer` user and rerun the verification commands +from step 6 with `$TOKEN_REVIEWER`. Every tool below is read-only, so the +difference is the binding's `ruleRestrictions` allow-list, not the +`readOnlyHint`: + +| Tool | Result | +| ------------------- | ------- | +| `list_issues` | allowed | +| `pull_request_read` | allowed | +| `search_code` | allowed | +| `get_file_contents` | denied | + +The reviewer is granted broad MCP verbs by the role, but the per-server binding +narrows the tool list to exactly the three names in `ruleRestrictions.tools`. + +## Step 8: Clean up + +Deleting the namespace removes the MCPServer, OIDC config, secret, and +namespaced `ToolhiveAuthorizationPolicy` in one shot. The two cluster-scoped +role bindings (and the custom `code-reviewer` role from the optional step) live +outside the namespace, so delete them explicitly: + +```bash +kubectl delete clusterplatformrolebinding \ + demo-admin-viewer-binding demo-reviewer-binding --ignore-not-found +kubectl delete clusterplatformrole code-reviewer --ignore-not-found +kubectl delete namespace authz-demo +``` + +## Next steps + +- Hand a namespace and a scoped policy surface to an application team with + [Namespace self-service](./namespace-self-service.mdx). +- See every field on the policy resource in the + [ToolhiveAuthorizationPolicy CRD reference](../reference/crds/toolhiveauthorizationpolicy.mdx). +- Understand what the operator compiles your `ToolhiveAuthorizationPolicy` into + with [Cedar policies](../../toolhive/concepts/cedar-policies.mdx). + +## Troubleshooting + +
+Tokens have the wrong `aud` claim + +Decode a fresh token with `jwt decode "$TOKEN" | jq .aud`. The value must match +`spec.oidcConfigRef.audience` on the MCPServer exactly. Entra emits the client +GUID by default; some tenants are configured to emit `api://` +instead. Update `audience` in `02-mcpserver-github.yaml` to whatever the JWT +actually contains, then re-apply. + +
+ +
+Tokens are missing the `roles` claim + +If `jwt decode` shows `.payload.roles` empty, the user isn't assigned to any app +role on the enterprise application. Open the enterprise app's **Users and +groups** page and add an assignment for the user. You can verify with +`az rest --method GET --uri "https://graph.microsoft.com/v1.0/users//appRoleAssignments"`. + +
+ +
+A `ToolhiveAuthorizationPolicy` doesn't reach the `Compiled` condition + +Inspect the resource for the failing condition and look at the operator events: + +```bash +kubectl -n authz-demo describe tap github-demo-rw-policy +``` + +The most common cause is a `roleRef` that names a `ClusterPlatformRole` that +doesn't exist (a typo, or the optional step's role wasn't applied yet). + +
+ +
+The viewer succeeds on a write tool + +The reader binding's `toolHintFilter` reads each tool's `readOnlyHint` +annotation, which the proxy only caches after `tools/list` has run on the +session. A standard MCP client (like the Inspector) runs `tools/list` during the +handshake, so the filter is always in effect. If you instead script raw JSON-RPC +calls, call `initialize`, `notifications/initialized`, and `tools/list` before +`tools/call`, or the filter sees an empty cache and the write slips through. + +
diff --git a/docs/platform/enterprise-cli/index.mdx b/docs/platform/enterprise-cli/index.mdx new file mode 100644 index 00000000..ad39f6a0 --- /dev/null +++ b/docs/platform/enterprise-cli/index.mdx @@ -0,0 +1,116 @@ +--- +title: Stacklok CLI +description: + Enterprise edition of the ToolHive CLI with OIDC sign-in and centralized + policy enforcement from the Enterprise Manager. +--- + +:::enterprise + +The Stacklok CLI is a component of Stacklok Enterprise. For a full comparison of +ToolHive Community and Stacklok Enterprise capabilities, see +[Stacklok Enterprise](../index.mdx). + +::: + +The Stacklok CLI is the enterprise edition of the +[ToolHive CLI](../../toolhive/guides-cli/index.mdx) (`thv`). Everything in the +open source `thv` works the same way. The Stacklok CLI adds OIDC authentication +to your Stacklok Enterprise platform and enforces the policies your +administrators define in the +[Enterprise Manager](../enterprise-manager/index.mdx). + +This page covers what the enterprise edition adds. For the base CLI workflows, +see the [ToolHive CLI](../../toolhive/guides-cli/index.mdx) guides. + +## How you get it + +Stacklok provides the CLI as part of your Enterprise subscription in two forms: + +- **Bundled with [Stacklok Desktop](../enterprise-desktop/index.mdx).** + Installing the desktop app installs and manages the CLI for you: it puts `thv` + on your `PATH` and keeps the CLI version matched to the app, the same way the + open source app does. See + [Access the CLI from the desktop app](../../toolhive/guides-ui/cli-access.mdx) + for how that works. Because the desktop app and the CLI share a session, + signing in to Stacklok Desktop also signs in the CLI, so this is the simplest + option. +- **Standalone `thv-enterprise` binary.** Install this yourself when you want + the CLI without the desktop app, such as on a server or in CI, and + authenticate it with `thv login`. + +Both are the enterprise build of `thv`. Command examples on this page use `thv`. + +## How it connects to the platform + +The CLI needs your platform URL before it can authenticate. It reads +`StacklokPlatformUrl` from managed preferences first (the macOS managed +preferences plist or the Windows registry), then falls back to the +`STACKLOK_PLATFORM_URL` environment variable. This is the same bootstrap +mechanism [Stacklok Desktop](../enterprise-desktop/index.mdx) uses, so a +fleet-wide managed preference configures both clients at once. + +From that URL, the CLI discovers the OIDC issuer, client ID, and scopes from the +platform's well-known configuration endpoint. You do not configure those by +hand. + +## Authentication + +The Stacklok CLI adds two commands for managing your platform session: + +- **`thv login`** opens a browser to complete the OAuth flow against your + platform. On success, the session is stored in your secrets store and shared + with Stacklok Desktop, so signing in from one signs in both. Login also + configures registry access, so later `thv registry` commands reuse the session + instead of opening a second browser flow. Re-run `thv login` at any time to + refresh the session and pick up administrator-side configuration changes. +- **`thv logout`** clears the cached session token and OAuth configuration. + +`thv login` discovers its settings from the platform, so you normally run it +with no flags. To override the discovered values, use `--client-id`, `--scopes`, +or `--callback-port`. + +If you installed the CLI through Stacklok Desktop and have already signed in +there, the CLI shares that session, so you can skip `thv login`. Run it when you +use the standalone binary, or to sign in from the terminal without opening the +desktop app. + +## Policy enforcement + +After you sign in, the CLI fetches signed configuration from the Enterprise +Manager and enforces the directives that apply to clients. Each directive +carries an `enforcement` level: `enforced` directives are mandatory, while +`default` directives set a value you can still override locally. This is the +same model [Stacklok Desktop](../enterprise-desktop/policy-enforcement.mdx) +applies. + +Two directives shape what the CLI can do: + +- **[Registry](../enterprise-manager/policies/registry.mdx).** When the registry + directive is enforced, the configured registry URL is locked. Attempts to + change it with `thv config set-registry` or `thv config unset-registry` are + blocked. +- **[Non-registry servers](../enterprise-manager/policies/non-registry-servers.mdx).** + When this directive is enforced to disallow them, the CLI blocks running MCP + servers that are not in your organization's approved registry. Use + `thv search` to find approved servers instead. + +For the full set of directives an administrator can configure, see +[Enterprise Manager policies](../enterprise-manager/policies/). + +## Degraded mode + +When the CLI cannot reach the Enterprise Manager, it keeps working from its +cached configuration and prints a `[ToolHive Policy Warning]` to standard error +so you know policy may be stale. If your administrator sets the +[degraded mode policy](../enterprise-manager/degraded-mode.mdx) to block new +installations, the CLI blocks creating new MCP servers until it reconnects. + +## Next steps + +- [ToolHive CLI guides](../../toolhive/guides-cli/index.mdx) for the base CLI + features +- [Enterprise Manager policies](../enterprise-manager/policies/) to see the + directives that govern the CLI +- [Configure platform identity](../enterprise-platform/configure-identity.mdx) + for the identity provider setup behind `thv login` diff --git a/docs/platform/enterprise-cloud-ui/administration/entries.mdx b/docs/platform/enterprise-cloud-ui/administration/entries.mdx new file mode 100644 index 00000000..f9ff0247 --- /dev/null +++ b/docs/platform/enterprise-cloud-ui/administration/entries.mdx @@ -0,0 +1,87 @@ +--- +title: Manage entries +description: + Publish, edit, and delete MCP server entries in a managed source from the + Cloud UI catalog. +--- + +Entries are the individual MCP server definitions that appear in the catalog. +This guide covers how to publish new entries, edit them, and delete them through +the Cloud UI. You need the `manageEntries` role. + +Only entries in a [managed source](./sources.mdx) can be edited from the Cloud +UI. Entries that the Registry Server syncs from a git, API, or Kubernetes source +are read-only here. Change those at their upstream source instead. + +## Publish a new entry + +1. Open the **MCP Servers** catalog and select the registry you want to publish + to. +2. Click **Create** to open the **Create server** dialog. +3. Fill in the entry details: + - **Name** - the server's unique name in reverse-DNS form, for example + `com.example/my-server`. + - **Description** - a short description of what the server does. + - **Version** - the entry's semantic version, for example `1.0.0`. +4. Choose the **Server source**. This mirrors how you configure a server in + Stacklok Desktop: + - **Local** - a packaged server that Stacklok Desktop runs locally. Choose + the package **Type** (`oci`, `npm`, `pypi`, `nuget`, or `mcpb`), the + **Transport** (`stdio`, `sse`, or `streamable-http`), the **Image** or + package reference, an optional **Port**, and optional command arguments. + - **Remote** - a server reachable over the network. Choose the **Transport** + (`streamable-http` or `sse`) and enter the **Remote URL**. +5. Set the **Claims** that control which users can see this server. Claims are + required on an entry, and must be a subset of your own claims. +6. Optionally configure how the server is run: + - **Environment variables** - add variables the server needs. Each can be + **Optional**, **Required**, or **Fixed (Uncustomizable)**, which locks the + value so it can't be changed at install time. + - **Secrets** - add secret variables such as API keys, and mark each as + required if needed. + - **Custom metadata** - add arbitrary `name=value` pairs to attach to the + entry. +7. Click **Create server** to publish the entry. + +The entry appears in the catalog immediately, visible to users whose JWT claims +match the entry's claims. + +## Edit an entry + +1. Open the entry's detail page. +2. Click **Edit** to reopen the entry form. +3. Update any field, including the server source, environment variables, + secrets, claims, or custom metadata. +4. Click **Save** to apply the changes. + +:::note + +Entries synced from a git, API, or Kubernetes source are read-only in the Cloud +UI. The **Edit** and **Delete** buttons appear only for entries in a managed +source. + +::: + +## Delete an entry + +1. Open the entry's detail page. +2. Click **Delete**. +3. Confirm the deletion in the dialog. + +Deleted entries are removed from the catalog immediately. This action can't be +undone. + +## Manage versions + +An entry can have multiple versions. The catalog shows the latest version by +default, and users switch between versions with the version selector on the +detail page. Publishing a new version of an existing entry updates the catalog +to show the latest version. + +## Next steps + +- [Browse the catalog](../browse-catalog.mdx) to see how entries appear to users +- [Manage sources](./sources.mdx) to control where entries come from +- [Manage registries](./registries.mdx) to organize entries into registries +- [Publish servers to the Registry Server](../../../toolhive/guides-registry/publish-servers.mdx) + for the entry format and the underlying admin API diff --git a/docs/platform/enterprise-cloud-ui/administration/index.mdx b/docs/platform/enterprise-cloud-ui/administration/index.mdx new file mode 100644 index 00000000..702eed7b --- /dev/null +++ b/docs/platform/enterprise-cloud-ui/administration/index.mdx @@ -0,0 +1,37 @@ +--- +title: Registry management +description: + Manage MCP server entries, sources, and registries through the Enterprise + Cloud UI. +--- + +The registry management features of the Enterprise Cloud UI let platform teams +manage the MCP server and skills catalog directly from the browser. What you can +do depends on your [role](../intro.mdx#roles): + +- **Entry management** - publish, update, and delete MCP server entries. +- **Source management** - create, update, and delete sources. +- **Registry management** - create, update, and delete registries. + +The **Registries** navigation item only appears for users with source or +registry management permissions. + +## Where does catalog data come from? + +Registries, sources, and entries come from one of two places, which determines +whether you can change them in the Cloud UI: + +| Origin | Where it's defined | How you change it | Who manages it | +| -------------- | ---------------------------------------------------------------------------------- | ---------------------------------------- | ---------------------------- | +| Config-managed | Registry Server [config file](../../../toolhive/guides-registry/configuration.mdx) | Update the config and redeploy | Platform operators | +| API-managed | Cloud UI or [Registry Server API](../../../toolhive/reference/registry-api.mdx) | Create, edit, and delete in the Cloud UI | Users with a management role | + +Config-managed resources are read-only in the Cloud UI. + +## Next steps + +Pick a topic: + +- [Entries](./entries.mdx) - publish, update, and delete MCP server entries +- [Sources](./sources.mdx) - manage where MCP server definitions come from +- [Registries](./registries.mdx) - manage registries that combine sources diff --git a/docs/platform/enterprise-cloud-ui/administration/registries.mdx b/docs/platform/enterprise-cloud-ui/administration/registries.mdx new file mode 100644 index 00000000..9abf5a79 --- /dev/null +++ b/docs/platform/enterprise-cloud-ui/administration/registries.mdx @@ -0,0 +1,76 @@ +--- +title: Manage registries +description: + Create, edit, and delete registries that combine sources into a catalog + audience in the Cloud UI. +--- + +A registry is a named collection of one or more [sources](./sources.mdx). It's +the catalog audience that users select from in the **MCP Servers** view. This +guide covers how to manage registries through the Cloud UI. You need the +`manageRegistries` role. + +## View registries + +Open **Registries** in the navigation bar. The list shows the registries you can +manage. Use the search box to filter by name. + +## Create a registry + +1. Click **Add a registry**. +2. Enter a **Registry name**. +3. Under **Registry sources**, select the sources this registry includes. A + registry can combine multiple sources. +4. Optionally add **Claims** to control which users can see and access the + registry. Claims operate in default-deny mode: a registry with no claims is + visible only to `superAdmin` users. +5. Click **Save**. + +### Registry and source claims + +Claims attach to both registries and sources, and they compose. Claims are +required on a source but optional on a registry. A common pattern is to apply a +broad claim at the registry layer and finer-grained claims at the +[source](./sources.mdx#source-and-registry-claims) layer, so a single registry +can present different contents to different roles. Because registry claims are +default-deny, a registry left with no claims is visible only to `superAdmin` +users. + +## Edit a registry + +1. Open the registry's actions menu and select **Edit registry**. +2. Update the included sources or claims. The registry name can't be changed. +3. Click **Save** to apply the changes. + +:::note + +[Config-managed](./index.mdx#where-does-catalog-data-come-from) registries are +read-only. Their actions menu shows **View registry** instead of **Edit +registry**. + +::: + +## Delete a registry + +1. Open the registry's actions menu and select **Delete registry**. +2. Confirm the deletion in the dialog. + +Deleting a registry doesn't delete the underlying entries. They remain available +in their sources and in any other registries that include those sources. +Config-managed registries can't be deleted from the Cloud UI. + +## View registry entries + +Select **View entries** from a registry's actions menu to see every entry the +registry presents. Entries are grouped by server and labeled with the source +they came from, and you can expand a server to see its earlier versions. Use the +source filter to narrow the list to a single source. + +## Next steps + +- [Manage sources](./sources.mdx) to control what feeds into your registries +- [Manage entries](./entries.mdx) to publish or update individual entries +- [Registry Server configuration](../../../toolhive/guides-registry/configuration.mdx) + for config-managed registry settings +- [Browse the catalog](../browse-catalog.mdx) to see how registry entries appear + to users diff --git a/docs/platform/enterprise-cloud-ui/administration/sources.mdx b/docs/platform/enterprise-cloud-ui/administration/sources.mdx new file mode 100644 index 00000000..c0c3d439 --- /dev/null +++ b/docs/platform/enterprise-cloud-ui/administration/sources.mdx @@ -0,0 +1,104 @@ +--- +title: Manage sources +description: + Create, edit, and delete the sources that supply MCP server entries to the + catalog. +--- + +Sources define where MCP server entries come from. A +[registry](./registries.mdx) is made up of one or more sources. This guide +covers how to manage sources through the Cloud UI. You need the `manageSources` +role. + +For the full definition of each source type and the options that aren't yet +editable in the UI, see +[Registry Server configuration](../../../toolhive/guides-registry/configuration.mdx). + +## View sources + +Open **Registries** in the navigation bar, then select **Sources** in the +sidebar. The list shows each source's name and type. Use the search box to +filter by name or type. + +## Source types + +When you create a source, choose one of four types: + +- **Git** - syncs entries from a registry file in a git repository. +- **API** - syncs entries from an upstream Registry Server that implements the + registry API. +- **Kubernetes** - auto-publishes entries from annotated, operator-managed + resources in a cluster namespace. +- **Managed** - holds entries created directly through the Cloud UI or the + Registry Server API. This is the source type you publish to when you + [manage entries](./entries.mdx). + +## Create a source + +1. With the **Sources** list open, click **Add a source**. +2. Enter a **Name** for the source. +3. Choose the **Type** and fill in its configuration: + - **Git** - the **Repository URL**, the **Branch** to sync from, and a **Sync + interval** (for example `1h`, `30m`, or `5m`). + - **API** - the **API Endpoint** and a **Sync interval**. + - **Kubernetes** - the **Namespace** to watch. + - **Managed** - no additional configuration. +4. Add the **Claims** that control which users can see the source's entries. + Claims are required on a source. +5. Click **Save**. + +The Registry Server begins syncing entries from the new source. Entries appear +in the catalog once the initial sync completes. + +:::note[Configured through the API or config file] + +Some source options aren't available in the Cloud UI yet: + +- Git sources sync from a branch. To pin a tag or commit, or to authenticate to + a private repository, configure the source through the Registry Server API or + configuration file. +- Sync filters that control which entries a source imports must also be set + through the API or configuration file. + +::: + +## Source and registry claims + +Claims attach to both sources and registries, and they compose. Claims are +required on a source but optional on a [registry](./registries.mdx). A common +pattern is to apply a broad claim at the registry layer and finer-grained claims +at the source layer, so a single registry can present different contents to +different roles. + +## Edit a source + +1. Open the source's actions menu and select **Edit source**. +2. Update the configuration. The source name can't be changed. +3. Click **Save** to apply the changes. + +:::note + +[Config-managed](./index.mdx#where-does-catalog-data-come-from) sources are +read-only. Their actions menu shows **View source** instead of **Edit source**. + +::: + +## Delete a source + +1. Open the source's actions menu and select **Delete source**. +2. Confirm the deletion in the dialog. + +Deleting a source removes the entries that originated from it. Config-managed +sources can't be deleted from the Cloud UI. + +## View source entries + +Select **View entries** from a source's actions menu to see every entry the +source provides, before any registry-level filtering or deduplication is +applied. + +## Next steps + +- [Manage registries](./registries.mdx) to combine sources into registries +- [Manage entries](./entries.mdx) to publish entries to a managed source +- [Browse the catalog](../browse-catalog.mdx) to see how entries appear to users diff --git a/docs/platform/enterprise-cloud-ui/ai-assistant.mdx b/docs/platform/enterprise-cloud-ui/ai-assistant.mdx new file mode 100644 index 00000000..7fe458b3 --- /dev/null +++ b/docs/platform/enterprise-cloud-ui/ai-assistant.mdx @@ -0,0 +1,96 @@ +--- +title: AI assistant +description: + Chat with AI models that call MCP tools directly from the Cloud UI catalog. +--- + +The AI assistant is a chat sidebar built into the Enterprise Cloud UI. It +connects to AI models through OpenRouter and can call tools exposed by the MCP +servers in your catalog, so you can interact with your infrastructure through +natural language without leaving the browser. + +## Prerequisites + +The AI assistant requires an OpenRouter API key configured server-side during +[deployment](./configure.mdx). The key is never exposed to the browser. + +## Open the assistant + +Toggle the assistant sidebar with the chat icon in the navigation bar, or press +**Cmd+B** (macOS) / **Ctrl+B** (Windows/Linux). The sidebar opens on the right +side of the page. + +## Select a model + +The model selector dropdown at the bottom of the sidebar lists models from +multiple providers (Anthropic, OpenAI, Google, Meta, and others). The list is +fetched from OpenRouter at runtime and filtered to only include models that +support tool calling. + +## Select MCP servers and tools + +The **MCP Server Selector** next to the model dropdown shows how many servers +and tools are currently active (for example, "3 Servers / 12 Tools"). Click it +to open the server selection dropdown: + +- Toggle individual servers on or off with the checkbox next to each server + name. +- Click the settings icon on a server to open the tool configuration modal, + where you can enable or disable individual tools, search tools by name, or use + **Enable All** / **Disable All**. + +When you send a message, the assistant can call any enabled tool. It decides +which tools to call based on your message, executes them through the MCP server, +and interprets the results in its response. + +## Send messages + +Type your message in the input area and press **Enter** or click the send +button. The assistant streams its response in real time. During streaming, the +send button becomes a stop button that cancels the current response. + +You can attach images and PDFs as context for your message (up to 5 files, 10 MB +each). Drag files into the input area or click the attachment button. + +## Chat history + +Conversations are stored in your browser (IndexedDB) and persist across +sessions. The **Chat history** dropdown in the sidebar header lets you: + +- Load a previous conversation. +- Delete individual conversations. +- Clear all conversations with **Clear All**. + +Each conversation records the model and selected MCP servers at the time of +creation. The conversation title is automatically set from the first message. + +## How tool execution works + +When the assistant decides to call a tool: + +1. The request goes to the Cloud UI backend (`/api/chat`), which authenticates + your session. +2. The backend connects to the selected MCP servers and executes the tool with + the parameters chosen by the model. +3. The tool result is returned to the model, which interprets it and generates a + formatted response. +4. A single request can chain up to 5 tool calls before the model produces its + final answer. + +All tool execution happens server-side. The browser never connects to MCP +servers directly. + +## Visibility + +An administrator can hide the assistant for all users via the Enterprise +Manager. See [Feature flags](./configure.mdx#feature-flags) in the Cloud UI +configuration for details. The assistant is on by default when no Enterprise +Manager is deployed. + +## Next steps + +- [Browse the catalog](./browse-catalog.mdx) to find and install MCP servers +- [Manage entries](./administration/entries.mdx) to publish new servers that the + assistant can use +- [Enterprise Manager policies](../enterprise-manager/policies/) to control + feature availability across your organization diff --git a/docs/platform/enterprise-cloud-ui/browse-catalog.mdx b/docs/platform/enterprise-cloud-ui/browse-catalog.mdx new file mode 100644 index 00000000..98b4ac9f --- /dev/null +++ b/docs/platform/enterprise-cloud-ui/browse-catalog.mdx @@ -0,0 +1,143 @@ +--- +title: Browse the catalog +description: + Search MCP servers and skills, view their details, and install them into + Stacklok Desktop or your AI clients. +--- + +The catalog is the main view of the Enterprise Cloud UI. It shows the MCP +servers and skills available to you, filtered by your permissions. From here you +can search, inspect a server's tools, and install it into Stacklok Desktop or +directly into an AI client. + +The Cloud UI has two catalog views, selectable from the navigation bar: + +- **MCP Servers** - the MCP servers published to your registries. +- **Skills** - the agent skills published to your registries. + +What you see in each view is filtered by your identity. The Registry Server +returns only the resources your JWT claims authorize, so the catalog never shows +servers or skills you can't access. See +[Claims-based visibility](./intro.mdx#claims-based-visibility) for details. + +## Browse MCP servers + +Select **MCP Servers** in the navigation bar to open the server catalog. The top +of the page includes: + +- **Registry selector** - a dropdown to switch between the registries available + to you. The Cloud UI defaults to the first available registry. +- **Search** - full-text search across server names and descriptions. +- **View toggle** - switch between **Grid view** (cards) and **List view** (a + table). + +Each server shows its name, description, and version. Servers that aggregate +other servers are tagged with a **Virtual MCP** badge. + +## Server details + +Select a server to open its detail page. The header shows the server name, its +publisher (the source it came from), and badges for the transport type, version, +and any environment variables. A version selector lets you switch between +available versions. + +The detail page has two tabs: + +- **About** - the full server description, a **View Repository** link when the + server declares a source repository, and a **Getting started** section with + the server's endpoint URL (with a copy button) and the **Add to client** + options. +- **Tools** - a table of every tool the server exposes, with each tool's name + and description. + +If you have the entry management role, **Edit** and **Delete** buttons also +appear on the detail page. See [Manage entries](./administration/entries.mdx). + +## Install a server in Stacklok Desktop + +The **Install** button opens Stacklok Desktop to the selected server's detail +page. Under the hood, the button triggers a deep link using the +`stacklok-enterprise-gui://` protocol: + +```text +stacklok-enterprise-gui://v1/open-registry-server-detail?serverName= +``` + +Stacklok Desktop receives this link and navigates to the server's detail page, +where you can review and install the server. If Stacklok Desktop isn't +installed, the Cloud UI shows a **Desktop app required** dialog prompting you to +contact your administrator. + +:::note + +The enterprise protocol `stacklok-enterprise-gui://` is separate from the open +source `toolhive-gui://` protocol, so both editions can coexist on the same +machine. + +::: + +## Add a server to an AI client + +The **Add to client** dropdown offers one-click integration with supported AI +clients. It's available on both the catalog list and the server detail page. The +dropdown also includes a **Copy URL** option that copies the server's endpoint +to your clipboard. + +### Cursor + +Opens Cursor with the MCP server pre-configured through a deep link: + +```text +cursor://anysphere.cursor-deeplink/mcp/install?name=&config= +``` + +### Visual Studio Code + +Opens VS Code with the MCP server pre-configured through a deep link: + +```text +vscode:mcp/install? +``` + +### Claude Code + +Copies a CLI command to your clipboard that you can paste into a terminal. For a +remote server, the command looks like: + +```bash +claude mcp add --transport http "" +``` + +## Browse skills + +Select **Skills** in the navigation bar to open the skills catalog. Skills are +agent skill packages published to your registries. The list shows each skill's +name and description, with a link to its source repository and an **Install** +button. + +Select a skill to open its detail page, which shows: + +- **Summary** - the skill's description, with badges for its version, + repository, and license. +- **SKILL.md** - the rendered contents of the skill's definition file, so you + can review what the skill does before installing it. + +From the skill detail page you can: + +- **Install** - opens Stacklok Desktop to install the skill, using the same deep + link mechanism as MCP servers: + + ```text + stacklok-enterprise-gui://v1/open-registry-skill-install?namespace=&skillName= + ``` + +- **Download** - downloads the skill package as a `.zip` archive so you can use + it outside Stacklok Desktop. +- **View Repository** - opens the skill's source repository. + +## Next steps + +- [AI assistant](./ai-assistant.mdx) to chat with AI models that call MCP tools + from the catalog +- [Manage entries](./administration/entries.mdx) to publish, update, or delete + servers in the catalog diff --git a/docs/platform/enterprise-cloud-ui/configure.mdx b/docs/platform/enterprise-cloud-ui/configure.mdx new file mode 100644 index 00000000..95a52664 --- /dev/null +++ b/docs/platform/enterprise-cloud-ui/configure.mdx @@ -0,0 +1,306 @@ +--- +title: Configure the Enterprise Cloud UI +description: + Configuration reference for the Enterprise Cloud UI component in the Stacklok + Enterprise platform chart. +--- + +The Enterprise Cloud UI runs as a Next.js application in your Kubernetes +cluster. It ships in the Stacklok Enterprise platform chart, the umbrella chart +that installs the platform. + +:::tip[Deploy the platform first] + +Install the Cloud UI with the +[platform chart](../enterprise-platform/deployment.mdx), which deploys it +alongside the other components. To run it in its own cluster, enable only this +component as described in +[Distributed deployments](../enterprise-platform/distributed-deployments.mdx). + +::: + +You enable the component with its `cloudUi.enabled` flag and set its +configuration under the `toolhive-cloud-ui` key in your platform `values.yaml`. + +## Prerequisites + +Before configuring, ensure you have: + +- A [Registry Server](../enterprise-platform/configure-registry-server.mdx) + deployed and reachable; its API URL is the `apiBaseUrl` value you set below +- An OIDC-compatible identity provider (Okta, Entra ID, or generic OIDC) + configured with a client application for the Cloud UI +- An [OpenRouter](https://openrouter.ai) API key, if you want to enable the + [AI assistant](./ai-assistant.mdx) +- Enterprise Cloud UI distribution access (container image and Helm chart, + provided by Stacklok during onboarding) + +## Configuration values + +Enable the Cloud UI and set its configuration: + +```yaml title="values.yaml" +# Enable the Cloud UI. +cloudUi: + enabled: true + +# Cloud UI configuration. +toolhive-cloud-ui: + # Required: Registry Server API URL + apiBaseUrl: 'https://registry.example.com' + + # Required: OIDC configuration + oidc: + issuerUrl: 'https://idp.example.com' + clientId: '' + clientSecret: '' + # scopes: 'openid,email,profile,offline_access' + + # Required: Better Auth session management + # Generate secret with: openssl rand -base64 32 + betterAuth: + secret: '' + url: 'https://cloud-ui.example.com' + + # Optional: Enterprise Manager for server-side enterprise config + # enterpriseManagerUrl: 'https://enterprise-manager.example.com' + + # Optional: external Redis for caching skill previews. + # redisUrl: 'redis://:@redis.example.com:6379/0' + + # Optional: White-label branding (see "White-label branding" below) + # branding: + # config: + # enabled: true + # json: + # logo_url: 'https://cdn.example.com/acme/logo.svg' + # favicon_url: 'https://cdn.example.com/acme/favicon.ico' + # design_tokens: + # colors: + # light: { primary: '#7a1a1a' } + # dark: { primary: '#b04545' } + # # Simple-override branding (bypasses ConfigMap mount). + # # Overridden by `branding.config.json` when both are set. + # name: 'Acme' + # logoUrl: 'https://cdn.example.com/acme/logo.svg' + # faviconUrl: 'https://cdn.example.com/acme/favicon.ico' + + # Additional env vars not yet exposed as structured keys above. + # env: + # - name: OPENROUTER_API_KEY + # value: '' +``` + +## Environment variables + +The Helm keys in the tables below are relative to the Cloud UI's values. In the +platform chart, nest them under the `toolhive-cloud-ui` key (for example, +`toolhive-cloud-ui.apiBaseUrl`). Each maps to the environment variable the Cloud +UI container reads. + +:::note + +`API_BASE_URL` and `ENTERPRISE_MANAGER_URL` can use in-cluster Service DNS +because the Cloud UI backend calls them server-side. `BETTER_AUTH_URL` and +`OIDC_ISSUER_URL` must be externally reachable URLs because the browser uses +them during the sign-in flow. + +::: + +### Required + +| Helm key | Env var | Description | +| ------------------- | -------------------- | ----------------------------------------------------------------------------------------------------- | +| `apiBaseUrl` | `API_BASE_URL` | Base URL of the Registry Server API (see [Registry API URL resolution](#registry-api-url-resolution)) | +| `oidc.issuerUrl` | `OIDC_ISSUER_URL` | OIDC issuer/discovery URL for your identity provider | +| `oidc.clientId` | `OIDC_CLIENT_ID` | OAuth2 client ID | +| `oidc.clientSecret` | `OIDC_CLIENT_SECRET` | OAuth2 client secret | +| `betterAuth.secret` | `BETTER_AUTH_SECRET` | Token encryption secret (min 32 chars, generate with `openssl rand -base64 32`) | +| `betterAuth.url` | `BETTER_AUTH_URL` | Public URL of the Cloud UI (used for auth callbacks) | + +### Optional (structured) + +| Helm key | Env var | Description | +| ------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `oidc.scopes` | `OIDC_SCOPES` | Comma-separated OIDC scopes. Overrides the defaults: `openid,email,profile,offline_access`. If set, include at least `openid` and `offline_access` to avoid breaking authentication and session refresh. | +| `enterpriseManagerUrl` | `ENTERPRISE_MANAGER_URL` | Enterprise Manager URL the Cloud UI server reads enterprise config from (feature-flag gating, plus registry and gateway URLs that override the env-var fallbacks). Server-side only, so this can be in-cluster Service DNS. When unset, the Cloud UI falls back to the env vars for those URLs. | +| `redisUrl` | `REDIS_URL` | External Redis connection URL. The Cloud UI caches unpacked skill previews so it doesn't re-fetch each skill's OCI artifact on every view. The chart does not deploy Redis; when unset or unreachable, previews are hidden rather than re-pulled. | +| `branding.config.enabled` | `BRANDING_CONFIG_PATH` | Enable file-based white-label branding. See [White-label branding](#white-label-branding). | +| `branding.config.json` | (none) | JSON payload with `logo_url`, `favicon_url`, and `design_tokens.colors`. See [White-label branding](#white-label-branding). | +| `branding.name` | `BRAND_NAME` | Simple-override app name. Bypasses the ConfigMap mount. Overridden by `branding.config.json` when both are set. | +| `branding.logoUrl` | `BRAND_LOGO_URL` | Simple-override logo URL. Bypasses the ConfigMap mount. | +| `branding.faviconUrl` | `FAVICON_URL` | Simple-override favicon URL. Bypasses the ConfigMap mount. | + +### Optional (additional env vars) + +Use the `env` array in `values.yaml` for env vars not exposed as structured +keys: + +| Env var | Description | +| -------------------- | ------------------------------------------------------------------------------------------------------------- | +| `OPENROUTER_API_KEY` | OpenRouter API key for the AI assistant | +| `TRUSTED_ORIGINS` | Comma-separated list of trusted origins for CORS and auth callbacks | +| `DATABASE_URL` | PostgreSQL connection string for session storage (required for large OIDC tokens, for example, with Entra ID) | + +## White-label branding + +Replace the Cloud UI's default logo, favicon, and theme colors with your +organization's branding so the application matches your internal visual +identity. + +The Helm chart delivers branding as a JSON file mounted into the Cloud UI +container. It writes the contents of `branding.config.json` into a ConfigMap, +mounts it at `/etc/branding/config.json`, and sets `BRANDING_CONFIG_PATH` to +point at it. Enable the `branding.config` block in your `values.yaml`: + +```yaml title="values.yaml" +branding: + config: + enabled: true + json: + logo_url: 'https://cdn.example.com/acme/logo.svg' + favicon_url: 'https://cdn.example.com/acme/favicon.ico' + design_tokens: + colors: + light: + primary: '#7a1a1a' + nav-background: '#1a1a2e' + dark: + primary: '#b04545' + nav-background: '#0d0d1a' +``` + +Changes to `branding.config.json` propagate to running pods automatically. The +kubelet refreshes the mounted file shortly after the ConfigMap update, without a +pod rollout. See +[Mounted ConfigMaps are updated automatically](https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically) +in the Kubernetes documentation for the underlying behavior. + +### Fields + +All fields are optional. Omitted fields fall back to the Cloud UI's defaults. + +| Field | Description | +| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `logo_url` | URL of the logo image (SVG, PNG, or WebP). Shown on the login screen and in the top navigation bar. | +| `favicon_url` | URL of the favicon (ICO, PNG, or SVG). | +| `design_tokens.colors` | Light and dark theme color tokens (see below). | +| `app_name` | Used as the logo's alt text. Does not rename the application elsewhere in the UI. Falls back to the `BRAND_NAME` environment variable, then to `ToolHive`. | + +### Simple overrides + +For quick name and logo changes without a ConfigMap, set `branding.name`, +`branding.logoUrl`, and `branding.faviconUrl` directly in `values.yaml`: + +```yaml title="values.yaml" +branding: + name: 'Acme' + logoUrl: 'https://cdn.example.com/acme/logo.svg' + faviconUrl: 'https://cdn.example.com/acme/favicon.ico' +``` + +These wire directly to env vars and skip the ConfigMap mount entirely. If you +also set `branding.config.json`, the config file takes precedence. + +### Color tokens + +Set CSS color values under `design_tokens.colors.light` and +`design_tokens.colors.dark` to override the Cloud UI's defaults for each theme. + +| Token | Controls | +| ---------------------------- | ---------------------------------------------- | +| `background` | Page background | +| `foreground` | Primary text color | +| `card` | Card background | +| `card-foreground` | Card text | +| `popover` | Popover and dropdown menu background | +| `popover-foreground` | Popover and dropdown menu text | +| `primary` | Primary button background | +| `primary-foreground` | Text on primary buttons | +| `secondary` | Secondary button background | +| `secondary-foreground` | Text on secondary buttons | +| `muted` | Muted surfaces (subtle backgrounds) | +| `muted-foreground` | Low-emphasis text (labels, captions) | +| `accent` | Hover and highlight background | +| `accent-foreground` | Text on accent surfaces | +| `destructive` | Destructive action background (errors, delete) | +| `destructive-foreground` | Text on destructive surfaces | +| `border` | Default border color | +| `input` | Form input background | +| `ring` | Focus ring color | +| `avatar-background` | User avatar fallback background | +| `nav-background` | Top navigation bar background | +| `nav-border` | Top navigation bar border | +| `nav-button-active-bg` | Active top-nav button background | +| `nav-button-active-text` | Active top-nav button text | +| `success` | Success indicator color | +| `warning` | Warning indicator color | +| `sidebar` | Sidebar background | +| `sidebar-foreground` | Sidebar text | +| `sidebar-primary` | Sidebar primary highlight background | +| `sidebar-primary-foreground` | Text on sidebar primary highlights | +| `sidebar-accent` | Sidebar hover background | +| `sidebar-accent-foreground` | Sidebar hover text | +| `sidebar-border` | Sidebar border | +| `sidebar-ring` | Sidebar focus ring | + +Any +[CSS color value](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value) +is accepted, for example: + +- Hex: `#7a1a1a` +- RGB: `rgb(122 26 26)` +- HSL: `hsl(0 65% 29%)` + +### Asset hosting + +The Cloud UI fetches `logo_url` and `favicon_url` server-side and serves them to +browsers itself. The URLs only need to be reachable from the Cloud UI's +deployment, not publicly. You can host your assets on an internal CDN or behind +your firewall. + +The `logo_url` and `favicon_url` must start with `http://` or `https://`. URLs +with any other scheme are rejected and the Cloud UI uses its default logo or +favicon instead. Using `http://` for an internal CDN is safe: the Cloud UI +fetches the images itself and re-serves them through its own origin, so the +browser always loads them over the same protocol it uses to access the Cloud UI +(typically HTTPS). + +## Registry API URL resolution + +The Cloud UI resolves the Registry Server API URL dynamically: + +1. If an [Enterprise Manager](../enterprise-manager/index.mdx) is configured and + the [registry policy](../enterprise-manager/policies/registry.mdx) has an + `api_url` value, the Cloud UI uses that URL. +2. Otherwise, the Cloud UI falls back to the `API_BASE_URL` environment + variable. + +This means you can omit `API_BASE_URL` entirely and let the Enterprise Manager +control the registry URL, or set `API_BASE_URL` as a fallback for when the +Enterprise Manager is unreachable. + +## Feature flags + +The [Enterprise Manager](../enterprise-manager/index.mdx) controls Cloud UI +feature visibility via the `assistant` directive. To hide the AI assistant for +all users, set it in the Enterprise Manager's section of your platform +`values.yaml`: + +```yaml title="values.yaml" +enterprise-manager: + enterpriseConfig: + assistant: + value: false + enforcement: 'enforced' +``` + +## Next steps + +- [Browse the catalog](./browse-catalog.mdx) to verify the deployment and + explore available MCP servers +- [AI assistant](./ai-assistant.mdx) to configure and use the chat sidebar +- [Registry management](./administration/) to manage entries, sources, and + registries +- [Configure the Enterprise Manager](../enterprise-manager/configure.mdx) to set + up feature flags and policy enforcement diff --git a/docs/platform/enterprise-cloud-ui/index.mdx b/docs/platform/enterprise-cloud-ui/index.mdx new file mode 100644 index 00000000..97b94019 --- /dev/null +++ b/docs/platform/enterprise-cloud-ui/index.mdx @@ -0,0 +1,29 @@ +--- +title: Enterprise Cloud UI +description: + Web console for discovering, installing, and managing MCP servers across your + organization. +--- + +import DocCardList from '@theme/DocCardList'; + +The Enterprise Cloud UI is the web-based management console for Stacklok +Enterprise. It connects to the +[Registry Server](../../toolhive/guides-registry/index.mdx) and gives your team +a single place to browse the MCP server and skills catalog, install servers into +AI clients, and manage registry sources and entries. + +## Where to start + +- **New to the Cloud UI?** Read the [Introduction](./intro.mdx) for + architecture, roles, and how the Cloud UI fits into the platform. +- **Ready to deploy?** Install it with the + [platform chart](../enterprise-platform/deployment.mdx), then see + [Configure the Enterprise Cloud UI](./configure.mdx) for its Helm values. +- **Already running?** Jump to [Browse the catalog](./browse-catalog.mdx) to + discover and install MCP servers, or [Registry management](./administration/) + to manage entries, sources, and registries. + +## Contents + + diff --git a/docs/platform/enterprise-cloud-ui/intro.mdx b/docs/platform/enterprise-cloud-ui/intro.mdx new file mode 100644 index 00000000..1b7dad02 --- /dev/null +++ b/docs/platform/enterprise-cloud-ui/intro.mdx @@ -0,0 +1,114 @@ +--- +title: Introduction +description: + What the Enterprise Cloud UI does, where it fits, and how roles control access + to catalog and administration features. +--- + +:::enterprise + +The Enterprise Cloud UI is a component of Stacklok Enterprise. For a full +comparison of ToolHive Community and Stacklok Enterprise capabilities, see +[Stacklok Enterprise](../index.mdx). + +::: + +The Enterprise Cloud UI gives platform teams and developers a web-based +interface for the MCP server and skills catalog managed by the +[Registry Server](../../toolhive/guides-registry/index.mdx). It extends the +[open source ToolHive Cloud UI](../../toolhive/guides-cloud-ui/intro.mdx) with +skills discovery and installation, one-click Stacklok Desktop integration, an AI +assistant, and full catalog management for platform teams. Use it to: + +- Browse and search the MCP server catalog +- Install servers into Stacklok Desktop with one click +- Add servers to AI clients (Claude Code, Cursor, Visual Studio Code) +- Chat with AI models that can call MCP tools directly +- Publish, update, and delete MCP server entries +- Manage registry sources and registries + +## Where it fits + +The Cloud UI is a Next.js application deployed in your Kubernetes cluster. It +reads from and writes to the Registry Server API and receives feature flags from +the [Enterprise Manager](../enterprise-manager/index.mdx). + +```mermaid +flowchart LR + Dev["Developer"] + Admin["Platform admin"] + + subgraph Enterprise["Stacklok Enterprise"] + direction TB + CloudUI["Enterprise Cloud UI"] + RS["Registry Server"] + EM["Enterprise Manager"] + end + + subgraph Clients["Stacklok clients"] + direction TB + Desktop["Stacklok Desktop"] + CC["Claude Code"] + Cursor["Cursor"] + end + + Dev -->|"browses catalog"| CloudUI + Admin -->|"manages entries"| CloudUI + CloudUI <-->|"API"| RS + CloudUI -.->|"feature flags"| EM + CloudUI -->|"deep link"| Desktop & CC & Cursor +``` + +## Roles + +Access to Cloud UI features is controlled by roles in your identity provider. +The Registry Server maps JWT claims to roles that determine what each user can +do. At a high level, there are four levels of access: + +- **Browse** - all authenticated users can search the catalog, view server + details, and copy endpoints. +- **Publish** - users with entry management permissions can publish, update, and + delete MCP server entries. +- **Administer sources** - users with source management permissions can create, + update, and delete sources. +- **Administer registries** - users with registry management permissions can + create, update, and delete registries. + +The Cloud UI only shows features you have access to. For example, the +**Registries** navigation item only appears for users with source or registry +management permissions. + +For details on how roles are configured, see the +[Registry Server authorization](../../toolhive/guides-registry/authorization.mdx) +guide. + +## Claims-based visibility + +The Registry Server filters API responses based on the claims in your JWT token. +Resources you are not authorized to see are never returned by the API, so they +never appear in the Cloud UI. This filtering happens server-side - the Cloud UI +does not perform client-side access control. + +## Feature flags + +The [Enterprise Manager](../enterprise-manager/index.mdx) can control Cloud UI +features through policy directives. Each directive carries an `enforcement` +level (`enforced` or `default`): + +| Feature | Controls | +| ----------- | ------------------------------------- | +| `assistant` | Show or hide the AI assistant sidebar | + +When `assistant` is set to `false` with `enforcement: "enforced"`, the sidebar +is hidden for all users. Other feature flags such as `playground` and +`non_registry_servers` apply to the desktop app only - see +[Enterprise Manager policies](../enterprise-manager/policies/) for details. + +## Next steps + +- [Deploy the platform](../enterprise-platform/deployment.mdx) to install the + Cloud UI in your Kubernetes cluster +- [Browse the catalog](./browse-catalog.mdx) to discover and install MCP servers +- [AI assistant](./ai-assistant.mdx) to chat with AI models that call MCP tools +- [Registry management](./administration/) to manage entries, sources, and + registries diff --git a/docs/platform/enterprise-desktop/deep-links.mdx b/docs/platform/enterprise-desktop/deep-links.mdx new file mode 100644 index 00000000..be7cc538 --- /dev/null +++ b/docs/platform/enterprise-desktop/deep-links.mdx @@ -0,0 +1,56 @@ +--- +title: Deep links +description: + How Stacklok Desktop receives install requests from the Enterprise Cloud UI. +--- + +Stacklok Desktop registers a custom protocol handler that lets the +[Enterprise Cloud UI](../enterprise-cloud-ui/index.mdx) trigger actions in the +desktop app. When you click **Install** on a server in the Cloud UI, your +browser opens Stacklok Desktop with the server pre-selected. + +## How it works + +1. The Cloud UI constructs a deep link URL using the + `stacklok-enterprise-gui://` protocol. +2. Your browser opens the URL, which the operating system routes to Stacklok + Desktop. +3. Stacklok Desktop parses the URL, navigates to the server detail page, and + opens the install modal. + +## Supported actions + +| Action | URL format | What it does | +| ------------------ | ----------------------------------------------------------------------------- | --------------------------------------------------- | +| Open server detail | `stacklok-enterprise-gui://v1/open-registry-server-detail?serverName=` | Opens the registry detail page for the named server | +| Install server | `stacklok-enterprise-gui://v1/open-registry-server-install?serverName=` | Opens the detail page and shows the install modal | + +The `serverName` parameter must contain only alphanumeric characters, hyphens, +underscores, and periods. + +## Protocol coexistence + +The enterprise protocol `stacklok-enterprise-gui://` is separate from the open +source protocol `toolhive-gui://`. Both editions can be installed on the same +machine without conflict. Deep links from the Enterprise Cloud UI always open +Stacklok Desktop, and deep links from the open source Cloud UI always open the +open source app. + +## Troubleshooting + +If clicking **Install** in the Cloud UI does not open Stacklok Desktop: + +- Verify that Stacklok Desktop is installed and has been launched at least once + (the first launch registers the protocol handler with the operating system). +- On Linux, verify that the `.desktop` file or Flatpak registration includes the + `x-scheme-handler/stacklok-enterprise-gui` MIME type. +- On macOS, if both editions are installed, verify that Stacklok Desktop is the + default handler for `stacklok-enterprise-gui://` URLs in **System Settings > + Default Apps**. + +## Next steps + +- [Browse the catalog](../enterprise-cloud-ui/browse-catalog.mdx) in the Cloud + UI to install servers into Stacklok Desktop +- [Policy enforcement](./policy-enforcement.mdx) to understand how policies may + affect which servers can be installed diff --git a/docs/platform/enterprise-desktop/index.mdx b/docs/platform/enterprise-desktop/index.mdx new file mode 100644 index 00000000..f59ed663 --- /dev/null +++ b/docs/platform/enterprise-desktop/index.mdx @@ -0,0 +1,32 @@ +--- +title: Stacklok Desktop +description: + Enterprise edition of the ToolHive desktop app with OIDC sign-in, policy + enforcement, and centralized management. +--- + +import DocCardList from '@theme/DocCardList'; + +Stacklok Desktop is the enterprise edition of the +[ToolHive desktop app](../../toolhive/guides-ui/index.mdx). It adds OIDC +authentication, centralized policy enforcement from the +[Enterprise Manager](../enterprise-manager/index.mdx), and deep link integration +with the [Enterprise Cloud UI](../enterprise-cloud-ui/index.mdx). + +All features of the open source ToolHive UI are available in Stacklok Desktop. +The guides in the [ToolHive UI](../../toolhive/guides-ui/index.mdx) section +cover those features. + +## Where to start + +- **New to Stacklok Desktop?** Read the [Introduction](./intro.mdx) for an + overview of what Stacklok Desktop adds. +- **Rolling it out to a fleet?** Follow the [Rollout guide](./rollout.mdx) to + push the signed app and platform URL to user devices via MDM. +- **Already running?** Jump to [Policy enforcement](./policy-enforcement.mdx) to + understand how policies control the UI, or [Deep links](./deep-links.mdx) for + Cloud UI integration. + +## Contents + + diff --git a/docs/platform/enterprise-desktop/intro.mdx b/docs/platform/enterprise-desktop/intro.mdx new file mode 100644 index 00000000..f921e475 --- /dev/null +++ b/docs/platform/enterprise-desktop/intro.mdx @@ -0,0 +1,85 @@ +--- +title: Introduction +description: + What Stacklok Desktop adds over the open source ToolHive desktop app, + including OIDC sign-in, policy enforcement, and managed updates. +--- + +:::enterprise + +Stacklok Desktop is a component of Stacklok Enterprise. For a full comparison of +ToolHive Community and Stacklok Enterprise capabilities, see +[Stacklok Enterprise](../index.mdx). + +::: + +Stacklok Desktop is a hardened edition of the +[ToolHive desktop app](../../toolhive/guides-ui/index.mdx) that adds centralized +management and security controls for organizations. Everything in the open +source app works the same. On top of that, it adds policy enforcement, +resilience when the Enterprise Manager is unreachable, managed updates, and deep +link integration with the Enterprise Cloud UI. It also bundles the +[Stacklok CLI](../enterprise-cli/index.mdx), so installing the app puts `thv` on +your `PATH` and shares a single sign-in session between the two. + +## Policy enforcement + +The [Enterprise Manager](../enterprise-manager/index.mdx) pushes policy +directives that control which features are visible and whether you can change +them. For example, your admin can hide the Playground tab, lock the registry +URL, or block custom MCP servers. See +[Policy enforcement](./policy-enforcement.mdx) for the full list of controls. + +## Degraded mode + +When Stacklok Desktop cannot reach the Enterprise Manager, it enters degraded +mode. A warning banner appears below the navigation bar: + +- **Grace period** (yellow) - the Enterprise Manager is unreachable but the + cached configuration is still valid. The app continues to work normally. +- **Degraded** (red) - the grace period has elapsed. Some enterprise features + may stop working depending on the + [degraded mode policy](../enterprise-manager/degraded-mode.mdx) configured by + your admin. + +Stacklok Desktop polls the Enterprise Manager every 5 seconds while in degraded +mode and recovers automatically when the connection is restored. + +## Managed updates + +Auto-update is disabled in Stacklok Desktop. Updates are distributed through +your organization's release cycle, not through the app's built-in update +mechanism. This gives your platform team control over which version is deployed +across the organization. + +## Deep link integration + +Stacklok Desktop can receive install requests from the +[Enterprise Cloud UI](../enterprise-cloud-ui/index.mdx). When you click +**Install** on a server in the Cloud UI, Stacklok Desktop opens with the server +pre-selected. See [Deep links](./deep-links.mdx) for details. + +The enterprise deep link protocol (`stacklok-enterprise-gui://`) is separate +from the open source protocol (`toolhive-gui://`), so both editions can coexist +on the same machine. + +## First launch and sign-in + +On first launch, the app connects to the +[Enterprise Manager](../enterprise-manager/index.mdx) to retrieve your +organization's policy configuration and OIDC settings. You see a sign-in screen +that initiates an OIDC/OAuth flow with your identity provider (Okta, Entra ID, +or any OIDC-compatible provider). After signing in, the app applies your +organization's policies and shows the main interface. + +You can sign out at any time from **Settings**. + +## Next steps + +- [Policy enforcement](./policy-enforcement.mdx) to understand how policies + control the UI +- [Deep links](./deep-links.mdx) for Cloud UI integration +- [Stacklok CLI](../enterprise-cli/index.mdx), which Stacklok Desktop installs + and keeps up to date +- [ToolHive UI guides](../../toolhive/guides-ui/index.mdx) for the base app + features diff --git a/docs/platform/enterprise-desktop/policy-enforcement.mdx b/docs/platform/enterprise-desktop/policy-enforcement.mdx new file mode 100644 index 00000000..8a332de8 --- /dev/null +++ b/docs/platform/enterprise-desktop/policy-enforcement.mdx @@ -0,0 +1,90 @@ +--- +title: Policy enforcement +description: + How Enterprise Manager policies control which features are visible and locked + in Stacklok Desktop. +--- + +The [Enterprise Manager](../enterprise-manager/index.mdx) pushes policy +directives to Stacklok Desktop that control which features are visible and +whether users can override them. This page describes how each directive affects +the desktop app. + +## How it works + +On startup, Stacklok Desktop fetches the policy configuration from the +Enterprise Manager. Each directive contains a `value` and an `enforcement` +level: + +- **`enforced`** - the setting is locked. The corresponding UI control is hidden + to prevent confusion, since the user cannot change it. +- **`default`** - the setting is applied as a default. The user can override it + locally. + +Stacklok Desktop caches the configuration and re-fetches it periodically. +Changes take effect the next time the app polls the Enterprise Manager. + +## Directives + +The following directives affect Stacklok Desktop: + +| Directive | What it controls | Hidden when | +| ---------------------- | -------------------------------------- | ---------------------------------------------- | +| `help_menu` | The help menu (**?** button) | `value` is `false` | +| `playground` | The Playground tab | `value` is `false` | +| `non_registry_servers` | Installing servers not in the registry | `value` is `false` | +| `registry` | The registry URL in Settings | `enforcement` is `enforced` and `value` is set | + +### Help menu + +When `help_menu` is set to `false`, the help button is hidden from the +navigation bar. + +### Playground + +When `playground` is set to `false`, the +[Playground tab](../../toolhive/guides-ui/playground.mdx) is removed from the +app. See +[Stacklok Desktop policies](../enterprise-manager/policies/desktop-app.mdx) for +how to configure this directive. + +### Custom MCP servers + +When `non_registry_servers` is set to `false`, users cannot install MCP servers +from sources outside the configured registry (Docker images, source packages, or +custom URLs). Only servers from the registry are available. See +[Non-registry servers policy](../enterprise-manager/policies/non-registry-servers.mdx) +for configuration details. + +### Registry + +When `registry` is set with `enforcement: "enforced"`, the registry settings tab +in **Settings** is hidden. The registry URL is locked to the value configured by +the admin, and users cannot change it. This also prevents the app from +redirecting users to the registry settings tab on first launch. + +When `enforcement` is `"default"`, the registry settings tab remains visible and +the configured URL is used as a default that users can override. + +See [Registry policy](../enterprise-manager/policies/registry.mdx) for +configuration details. + +## Startup behavior + +When Stacklok Desktop launches: + +1. The app shows a loading screen ("Starting Stacklok Desktop") while it fetches + the policy configuration. +2. If the user is not authenticated, the app redirects to the sign-in screen. +3. After authentication, the app applies the policy directives and shows the + main interface with the appropriate features visible. + +If the Enterprise Manager is unreachable during startup, Stacklok Desktop enters +[degraded mode](./intro.mdx#degraded-mode) and uses the cached configuration. + +## Next steps + +- [Enterprise Manager policies](../enterprise-manager/policies/) to configure + the directives described on this page +- [Degraded mode](../enterprise-manager/degraded-mode.mdx) to control client + behavior when the Enterprise Manager is unreachable diff --git a/docs/platform/enterprise-desktop/rollout.mdx b/docs/platform/enterprise-desktop/rollout.mdx new file mode 100644 index 00000000..120d6c83 --- /dev/null +++ b/docs/platform/enterprise-desktop/rollout.mdx @@ -0,0 +1,350 @@ +--- +title: Roll out Stacklok Desktop +description: + Distribute Stacklok Desktop to user devices via MDM, with the platform URL + pre-configured so users sign in without typing it. +--- + +This page covers a silent, fleet-wide rollout of Stacklok Desktop to user +devices via your mobile device management (MDM) platform. The goal is that a +user opens the app for the first time and lands directly on the single sign-on +(SSO) screen, with no prompt to enter a platform URL and no elevated privileges +required. + +The Stacklok CLI reads the same managed preference when it's installed alongside +the desktop app. See [Stacklok CLI](../enterprise-cli/index.mdx) for +CLI-specific rollout notes. + +## How the platform URL bootstrap works + +On first launch, Stacklok Desktop needs to know which +[Enterprise Manager](../enterprise-manager/index.mdx) to talk to. It learns the +URL from one of two sources, checked in this order: + +1. The MDM-managed preference (see the per-OS paths below). This is the + recommended path for fleet rollouts. +2. The `STACKLOK_PLATFORM_URL` environment variable. Useful for local + development or single-user installs; not appropriate for fleet rollout. + +If neither is set, the app prompts for the URL at first launch. The goal of the +rollout is to never reach that prompt. + +The key name and per-OS managed-preference location are: + +| OS | Path | Key | +| ------- | ---------------------------------------------------------- | --------------------- | +| macOS | `/Library/Managed Preferences/com.stacklok.toolhive.plist` | `StacklokPlatformUrl` | +| Windows | `HKLM\SOFTWARE\Policies\Stacklok\ToolHive` | `StacklokPlatformUrl` | + +The macOS path is the standard Apple managed-preferences location delivered by a +Configuration Profile. The Windows path follows the conventional +`HKLM\SOFTWARE\Policies\\` layout used by Microsoft Edge, +Google Chrome, Mozilla Firefox, and Docker Desktop, delivered as a registry +policy by your MDM. + +## Prerequisites + +Before you start, ensure you have: + +- An active Stacklok Enterprise license and access to the Stacklok-provided + distribution channel for the desktop installer. Both are issued during + onboarding. +- The browser-reachable Cloud UI URL of your Stacklok Enterprise deployment (the + same value you set as `betterAuth.url` when deploying the + [Enterprise Cloud UI](../enterprise-cloud-ui/configure.mdx)). This is the + value of `` throughout this page. +- Administrative access to your MDM with permission to upload apps and + managed-configuration policies. +- A target device group scoped to the users who should receive the app. Do not + deploy to all devices. + +## Obtain the installer + +Stacklok provides the signed desktop installer through your enterprise +distribution channel. Contact your Stacklok representative if you do not have +access yet. + +You receive a per-architecture artifact: + +| OS | Artifact | +| ------- | --------------------------------------------------- | +| macOS | `StacklokDesktop--arm64.dmg` (Apple Silicon) | +| macOS | `StacklokDesktop--x64.dmg` (Intel) | +| Windows | `StacklokDesktop.Setup.exe` | + +The macOS `.dmg` is signed with Stacklok's Developer ID Application certificate, +notarized by Apple, and stapled. The Windows `.exe` is signed via Azure Trusted +Signing. Neither requires a Gatekeeper or SmartScreen bypass on managed devices. + +:::warning[Per-user install on Windows] + +The Windows installer is a Squirrel installer that lays the app down under +`%LOCALAPPDATA%\Programs\StacklokDesktop\`, not `C:\Program Files\`. Your MDM +detection rules **must** target the per-user path. Rules that look for an +executable under `%ProgramFiles%\...` incorrectly report the install as failed. + +::: + +## Push the platform URL on macOS + +Stacklok Desktop reads `StacklokPlatformUrl` from the managed-preferences domain +`com.stacklok.toolhive` at first launch. You deliver the value by uploading a +Configuration Profile to your MDM. + +### Configuration Profile template + +Save the following as `com.stacklok.toolhive.mobileconfig` on your admin +workstation, replacing `` with your deployment URL and +`` with your organization name. Do not include a trailing slash on +the URL. + +```xml title="com.stacklok.toolhive.mobileconfig" + + + + + PayloadType + Configuration + PayloadVersion + 1 + PayloadIdentifier + com.stacklok.toolhive.managed-settings + PayloadUUID + 1864D77D-1931-474A-B099-2970676AA3FF + PayloadDisplayName + Stacklok Desktop - Managed Settings + PayloadOrganization + + PayloadScope + System + PayloadContent + + + PayloadType + com.stacklok.toolhive + PayloadVersion + 1 + PayloadIdentifier + com.stacklok.toolhive.managed-settings.payload + PayloadUUID + C248058F-2905-46E8-9AB7-9F0807BD1166 + PayloadDisplayName + Stacklok Desktop managed preferences + StacklokPlatformUrl + + + + + +``` + +Despite the `.mobileconfig` extension, this is the universal Apple Configuration +Profile format and applies to macOS. Validate the file with +`plutil -lint com.stacklok.toolhive.mobileconfig` before uploading. + +### Upload to your MDM (macOS) + +Upload the Configuration Profile alongside the signed `.dmg`, and scope both to +the device group that should receive the app. The exact menu paths differ per +MDM; consult your vendor's documentation for screenshots and edge cases. Worked +examples for two commonly used MDMs: + +**Jamf Pro** + +1. Upload the `.dmg` under **Computers** > **Management Settings** > **Computer + Management** > **Packages** > **New**. +2. Create the deployment policy under **Computers** > **Policies** > **New**, + add the package, and scope to the target smart group. +3. Upload the Configuration Profile under **Computers** > **Configuration + Profiles** > **Upload Profile**, and scope to the same smart group. + +Vendor docs: [Jamf Pro documentation](https://learn.jamf.com/). + +**Microsoft Intune** + +1. Upload the `.dmg` under **Apps** > **macOS** > **Add** > **macOS app (DMG)**, + fill in the app info (publisher `Stacklok`, minimum OS `11.0`), and assign to + the target Entra ID group. +2. Upload the Configuration Profile under **Devices** > **Configuration** > + **Create profile**, choose platform **macOS** and profile type + **Templates** > **Custom**, upload the `.mobileconfig`, and assign to the + same group. + +Intune's macOS DMG path requires the Intune management agent installed on each +device with Full Disk Access granted on macOS 13+ (System Settings > Privacy & +Security > Full Disk Access). + +Vendor docs: +[Add an unmanaged macOS DMG app to Microsoft Intune](https://learn.microsoft.com/intune/). + +## Push the platform URL on Windows + +On Windows the equivalent of macOS managed preferences is the registry. The +desktop app reads `StacklokPlatformUrl` from +`HKLM\SOFTWARE\Policies\Stacklok\ToolHive` at runtime. The hive is `HKLM` +(machine-scoped); do not put the value under `HKCU`. + +The recommended delivery artifact is a `.reg` file. An ADMX/ADML policy template +pair is also available from Stacklok during onboarding for MDMs that render form +UI from ingested ADMX (Microsoft Intune via Import ADMX, Workspace ONE via the +ADMX Profile payload). Both artifacts write the same key and value, so a mixed +fleet can use whichever artifact suits each MDM; the result on each device is +identical. + +### Registry template + +Save the following as `ToolHive.reg` on your admin workstation, replacing +`` with your deployment URL: + +```text title="ToolHive.reg" +Windows Registry Editor Version 5.00 + +[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Stacklok\ToolHive] +"StacklokPlatformUrl"="" +``` + +### Upload to your MDM (Windows) + +Upload the signed `.exe` alongside the registry policy, and scope both to the +device group that should receive the app. Worked examples for two commonly used +MDMs: + +**Microsoft Intune** + +1. Wrap the installer. Intune's Win32 app type requires `.exe` installers to be + wrapped into `.intunewin` packages with the + [Microsoft Win32 Content Prep Tool](https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool). + + ```cmd + IntuneWinAppUtil.exe -c .\source -s StacklokDesktop.Setup.exe -o .\output + ``` + +2. Upload the wrapped app under **Apps** > **Windows** > **Add** > **Windows app + (Win32)**. Set the install command to `StacklokDesktop.Setup.exe --silent`. + Set the detection rule to look for + `%LOCALAPPDATA%\Programs\StacklokDesktop\StacklokDesktop.exe` (the per-user + path, not `Program Files`). +3. Push the registry value. Either import the ADMX/ADML pair under **Devices** > + **Configuration** > **Import ADMX**, then create a Settings Catalog profile + that sets **Stacklok platform URL** to ``; or push the `.reg` + values via a remediation script that runs `reg import` against a pre-staged + copy of `ToolHive.reg`. +4. Assign both to the target Entra ID group. + +Vendor docs: +[Win32 app management in Microsoft Intune](https://learn.microsoft.com/intune/). + +**Workspace ONE / Omnissa** + +1. Upload the installer under **Resources** > **Apps** > **Native** > **Add + Application** > **Windows** > **Internal**, upload + `StacklokDesktop.Setup.exe`. Set the install command to + `StacklokDesktop.Setup.exe --silent`, the uninstall command to + `Update.exe --uninstall -s`, and the detection rule to file path + `%LOCALAPPDATA%\Programs\StacklokDesktop\StacklokDesktop.exe`. +2. Push the registry value. Either upload the ADMX/ADML pair under + **Resources** > **Profiles** > **Add Profile** > **Windows Desktop** > + **Administrative Templates** and enter `` in the form field, or + use Registry Automation to import the `.reg` file directly. +3. Assign both to the target smart group. + +Vendor docs: [Workspace ONE UEM documentation](https://docs.omnissa.com/). + +## Verify the rollout + +On a test device in the scoped group, after the MDM push completes: + +**macOS** + +```bash +# App installed +ls /Applications/StacklokDesktop.app + +# Configuration Profile applied +profiles list | grep -i toolhive + +# Managed preference landed +defaults read "/Library/Managed Preferences/com.stacklok.toolhive.plist" \ + StacklokPlatformUrl +``` + +The last command should print your ``. Open the app from +`/Applications`; it should land directly on the SSO sign-in screen with no +platform-URL prompt. + +**Windows** + +```cmd +:: App installed (per-user path) +dir "%LOCALAPPDATA%\Programs\StacklokDesktop\StacklokDesktop.exe" + +:: Registry policy applied +reg query "HKLM\SOFTWARE\Policies\Stacklok\ToolHive" /v StacklokPlatformUrl +``` + +The `reg query` output should show your ``. Open Stacklok Desktop +from the Start menu; it should land directly on the SSO sign-in screen with no +platform-URL prompt. + +## Managed updates + +Auto-update is disabled in Stacklok Desktop. New versions reach user devices +through your organization's release cycle, not through the app's built-in update +mechanism. This gives your platform team control over which version is deployed +across the organization. + +When Stacklok publishes a new release, repeat the +[Obtain the installer](#obtain-the-installer) step for the new version and +update the deployment policy in your MDM to point at the new artifact. The +managed-preference Configuration Profile and registry policy do not need to +change between releases unless your platform URL changes. + +## Next steps + +- [Policy enforcement](./policy-enforcement.mdx) to understand which features + the Enterprise Manager controls once the app is running. +- [Deep links](./deep-links.mdx) for Enterprise Cloud UI integration. +- [Configure identity](../enterprise-platform/configure-identity.mdx) if you + have not already set up OIDC for your deployment. + +## Troubleshooting + +
+User is prompted for the platform URL at first launch + +The managed preference did not land. On macOS, run +`profiles show -identifier com.stacklok.toolhive.managed-settings` on the user's +device. On Windows, run +`reg query "HKLM\SOFTWARE\Policies\Stacklok\ToolHive" /v StacklokPlatformUrl`. +If either returns nothing, re-check the MDM scope and the device's check-in +status. + +
+ +
+MDM reports install success on Windows but the app does not appear + +The detection rule is targeting `Program Files` instead of the per-user path. +Repoint the rule at +`%LOCALAPPDATA%\Programs\StacklokDesktop\StacklokDesktop.exe`. + +
+ +
+MDM reports install success on macOS but the app does not launch + +The notarization staple was stripped by an MDM repackaging step. Confirm with +`xcrun stapler validate "/Applications/StacklokDesktop.app"` on the user's +device. If validation fails, redeploy the original `.dmg` without repackaging. + +
+ +
+Logs to collect when escalating to Stacklok support + +- macOS: `~/Library/Logs/Stacklok Desktop/main.log` plus the output of + `profiles list` and the `defaults read` command above. +- Windows: `%APPDATA%\Stacklok Desktop\logs\main.log` plus the output of the + `reg query` command above. + +
diff --git a/docs/platform/enterprise-manager/configure.mdx b/docs/platform/enterprise-manager/configure.mdx new file mode 100644 index 00000000..2a1713b8 --- /dev/null +++ b/docs/platform/enterprise-manager/configure.mdx @@ -0,0 +1,115 @@ +--- +title: Configure the Enterprise Manager +description: + Configuration reference for the Enterprise Manager component in the Stacklok + Enterprise platform chart. +--- + +The Enterprise Manager runs as a Kubernetes service. It ships in the Stacklok +Enterprise platform chart, the umbrella chart that installs the platform. + +:::tip[Deploy the platform first] + +Install the Enterprise Manager with the +[platform chart](../enterprise-platform/deployment.mdx), which deploys it +alongside the other components. To run it in its own cluster, enable only this +component as described in +[Distributed deployments](../enterprise-platform/distributed-deployments.mdx). + +::: + +You enable the component with its `enterpriseManager.enabled` flag and set its +configuration under the `enterprise-manager` key in your platform `values.yaml`. + +## Prerequisites + +Before configuring, ensure you have: + +- An OIDC-compatible identity provider (Okta, Entra ID, or generic OIDC) + configured with a client application for Stacklok clients to use +- Enterprise Manager distribution access, which includes the container image and + Helm chart (provided by Stacklok during onboarding) + +## Generate a signing key + +The Enterprise Manager signs every policy envelope with an EC P-256 key. Clients +verify the signature against the public key the Enterprise Manager advertises at +its discovery endpoint, so a cached envelope can be trusted as genuine and +untampered even when the server is temporarily unreachable. Generate one if you +don't already have it: + +```bash +openssl ecparam -name prime256v1 -genkey -noout -out signing.pem +``` + +Store it as a Kubernetes Secret in the `stacklok-system` namespace: + +```bash +kubectl create secret generic enterprise-manager-signing-key \ + --from-file=signing.pem=./signing.pem \ + -n stacklok-system +``` + +## Configuration values + +Enable the Enterprise Manager and set its configuration: + +```yaml title="values.yaml" +# Enable the Enterprise Manager. +enterpriseManager: + enabled: true + +# Enterprise Manager configuration. +enterprise-manager: + # IdP / OIDC configuration + idpConfig: + # OIDC issuer URI, the base URL of your identity provider + issuer: 'https://idp.example.com' + # Expected JWT audience claim + audience: 'enterprise-manager' + # OAuth2 scope required to call the config endpoint + requiredScope: 'toolhive:config:read' + # IdP type: "entra", "okta", or "generic" + idpType: 'generic' + + # Signing configuration + signingConfig: + # Name of the Secret created in the prerequisites step + existingSecret: 'enterprise-manager-signing-key' + # How long a signed envelope is valid for + envelopeTTL: '24h' + # How often clients should poll for a new envelope + refreshInterval: '5m' + + # Public URL where clients reach the Enterprise Manager. + # Clients use this to discover the authorization server (RFC 9728). + resourceURL: 'https://config.example.com' + + # OIDC client ID for the Stacklok CLI and Stacklok Desktop. + # Configure a public (PKCE, no client secret) application in your IdP. + clientID: '' + + # Enterprise policy configuration + enterpriseConfig: + # Enforce a specific MCP registry URL + registry: + value: + api_url: 'https://registry.example.com' + enforcement: 'enforced' + # Block MCP servers not listed in the registry + non_registry_servers: + value: false + enforcement: 'enforced' + # Client behavior when the Enterprise Manager is unreachable + degraded_mode: + policy: 'block_new' + grace_period: '24h' + message: 'Enterprise Manager unreachable. Contact your administrator.' +``` + +## Next steps + +- [Configure policies](./policies/) to control client behavior across your + organization +- [Configure degraded mode](./degraded-mode.mdx) to define how clients behave + when the Enterprise Manager is unreachable diff --git a/docs/platform/enterprise-manager/degraded-mode.mdx b/docs/platform/enterprise-manager/degraded-mode.mdx new file mode 100644 index 00000000..93ac3bb0 --- /dev/null +++ b/docs/platform/enterprise-manager/degraded-mode.mdx @@ -0,0 +1,71 @@ +--- +title: Degraded mode +description: + Configure how Stacklok clients behave when the Enterprise Manager is + unreachable. +--- + +Degraded mode governs how Stacklok clients behave when the Enterprise Manager is +unreachable, for example during a network partition, server maintenance, or an +outage. You can configure a stricter policy to prevent unapproved activity +during outages. + +Unlike the policy directives described in [Policies](./policies/), degraded mode +does not carry an `enforcement` field. It controls client fallback behavior: +what happens when the server cannot be reached to enforce anything at all. + +## Modes + +| Mode | Behavior | +| ----------- | -------------------------------------------------------------------------------------- | +| `warn` | All operations proceed normally. A warning is shown to the user. | +| `block_new` | New MCP server installations are blocked. Servers already running continue to operate. | + +## Configuration + +Add the `degraded_mode` block to your enterprise configuration. + +For Helm deployments, set the values in `values.yaml`: + +```yaml title="values.yaml" +enterpriseConfig: + degraded_mode: + # One of: warn, block_new + policy: 'block_new' + # Optional: how long to wait before the policy takes effect after the + # server becomes unreachable. Go duration string (e.g., "24h", "30m"). + grace_period: '24h' + # Optional: message shown to users when degraded mode is active + message: 'Enterprise Manager unreachable. Contact your administrator.' +``` + +For manual Kubernetes deployments, set the same fields in your +`enterprise-config.json` ConfigMap: + +```json title="enterprise-config.json" +{ + "degraded_mode": { + "policy": "block_new", + "grace_period": "24h", + "message": "Enterprise Manager unreachable. Contact your administrator." + } +} +``` + +## Grace period + +The `grace_period` field delays the policy taking effect after the server +becomes unreachable. During the grace period, clients operate as if in `warn` +mode regardless of the configured policy. This prevents brief network +interruptions from immediately blocking developer workflows. + +For example, with `grace_period: "24h"` and `policy: "block_new"`, clients +continue working normally for 24 hours after losing contact with the server. +After 24 hours, new server installations are blocked. + +## Next steps + +- [Configure policies](./policies/) to control client behavior when the server + is reachable +- [Deploy the platform](../enterprise-platform/deployment.mdx) if you have not + already done so diff --git a/docs/platform/enterprise-manager/index.mdx b/docs/platform/enterprise-manager/index.mdx new file mode 100644 index 00000000..4f751c40 --- /dev/null +++ b/docs/platform/enterprise-manager/index.mdx @@ -0,0 +1,28 @@ +--- +title: Enterprise Manager +description: + Deploy and configure the Stacklok Enterprise Manager for centralized policy + enforcement across Stacklok clients. +--- + +import DocCardList from '@theme/DocCardList'; + +The Enterprise Manager is the central policy-distribution component of Stacklok +Enterprise. It runs as a Kubernetes service and delivers signed configuration +envelopes to Stacklok clients, giving your organization centralized control over +registry access, server permissions, telemetry, and client behavior. + +## Where to start + +- **New to the Enterprise Manager?** Read the [Introduction](./intro.mdx) for + architecture, enforcement model, and binary access. +- **Ready to deploy?** Install it with the + [platform chart](../enterprise-platform/deployment.mdx), then see + [Configure the Enterprise Manager](./configure.mdx) for its Helm values. +- **Already running?** Jump to [Policies](./policies/) to configure enforcement + rules, or [Degraded mode](./degraded-mode.mdx) to control client behavior + during outages. + +## Contents + + diff --git a/docs/platform/enterprise-manager/intro.mdx b/docs/platform/enterprise-manager/intro.mdx new file mode 100644 index 00000000..38d66c57 --- /dev/null +++ b/docs/platform/enterprise-manager/intro.mdx @@ -0,0 +1,89 @@ +--- +title: Introduction +description: + What the Enterprise Manager does, where it fits, and how enforcement works + across Stacklok clients. +--- + +:::enterprise + +The Enterprise Manager is a component of Stacklok Enterprise. For a full +comparison of ToolHive Community and Stacklok Enterprise capabilities, see +[Stacklok Enterprise](../index.mdx). + +::: + +The Enterprise Manager gives platform and security teams centralized control +over how the Stacklok CLI and Stacklok Desktop (the enterprise editions of the +ToolHive CLI and desktop app) behave across your organization. Use it to: + +- Pin all clients to your internal MCP registry +- Block MCP servers that are not listed in that registry +- Standardize OpenTelemetry collector configuration +- Tailor the Stacklok Desktop experience (for example, hide the Playground tab) +- Define how clients behave when the Enterprise Manager is unreachable + +## Where it fits + +The Enterprise Manager runs as a service in your Kubernetes cluster. Clients +authenticate, fetch their configuration, and poll again on a refresh interval +you control, so policy updates propagate across your fleet without manual client +changes. The [Enterprise Cloud UI](../enterprise-cloud-ui/index.mdx) also +consumes feature flags from the Enterprise Manager to control UI features like +the AI assistant. + +```mermaid +flowchart LR + Admin["Platform admin"] + + subgraph Enterprise["Stacklok Enterprise"] + direction TB + Config@{ shape: doc, label: "enterprise-config.json" } + EM["Enterprise Manager"] + Config --> EM + end + + subgraph Clients["Stacklok clients"] + direction TB + CLI["Stacklok CLI"] + UI["Stacklok Desktop"] + end + + Admin -->|"writes policies"| Config + EM <-->|"signed policies"| CLI & UI +``` + +## Enforcement levels + +Every policy directive carries an `enforcement` field — either `enforced` +(mandatory, cannot be overridden locally) or `default` (advisory, can be +overridden). See [Enforcement levels](./policies/#enforcement-levels) for +details. + +## How clients connect + +Clients bootstrap from a single well-known URL: + +```text +GET /.well-known/toolhive-configuration +``` + +That document returns everything a client needs to authenticate and fetch +configuration: the config endpoint, the JWKS URI used to verify envelope +signatures, and the OIDC issuer, client ID, and scopes for the PKCE auth flow. +No out-of-band credential distribution is required. You share the bootstrap URL +and clients handle the rest. + +Each configuration envelope is signed with an EC P-256 key, tagged with an ETag +for efficient caching, stamped with `issued_at` / `not_after` validity +timestamps, and includes the refresh interval that tells the client when to poll +next. + +## Next steps + +- [Deploy the platform](../enterprise-platform/deployment.mdx) to install the + Enterprise Manager in your Kubernetes cluster +- [Configure policies](./policies/) to control client behavior across your + organization +- [Configure degraded mode](./degraded-mode.mdx) to define client behavior when + the Enterprise Manager is unreachable diff --git a/docs/platform/enterprise-manager/policies/build-env.mdx b/docs/platform/enterprise-manager/policies/build-env.mdx new file mode 100644 index 00000000..ed11004b --- /dev/null +++ b/docs/platform/enterprise-manager/policies/build-env.mdx @@ -0,0 +1,63 @@ +--- +title: Build environment policy +description: + Inject environment variables into all MCP server containers without modifying + individual server definitions. +--- + +Use this directive to inject environment variables into every MCP server +container managed by ToolHive. Common uses include configuring HTTP proxies so +containers can reach the internet through your corporate proxy, pointing +containers at internal endpoints, or setting org-wide values that every server +needs. + +You'll need the Enterprise Manager already [deployed](../configure.mdx) and +reachable by clients. + +## Configure the build environment directive + +Add the `build_env` directive to your enterprise configuration. The `value` is a +flat map of environment variable names to string values. Pick an +[enforcement level](./index.mdx#enforcement-levels) to match your use case. + +```yaml title="values.yaml" +enterpriseConfig: + build_env: + value: + HTTP_PROXY: 'http://proxy.example.com:3128' + HTTPS_PROXY: 'http://proxy.example.com:3128' + NO_PROXY: 'localhost,127.0.0.1,.internal.example.com' + enforcement: 'enforced' +``` + +Use `enforced` when the variables must be present for containers to function, +for example when all outbound traffic must go through a corporate proxy. Use +`default` when you want to provide org-wide defaults that individual developers +or teams can override locally. + +After updating your configuration, +[apply the change](./index.mdx#apply-policy-changes). + +## Variations + +### Advisory defaults + +Provide org-wide environment defaults while allowing local overrides: + +```yaml title="values.yaml" +enterpriseConfig: + build_env: + value: + MY_INTERNAL_API: 'https://api.internal.example.com' + enforcement: 'default' +``` + +## Next steps + +- [CA certificate policy](./ca-certificate.mdx) to inject a custom CA + certificate into MCP containers +- [Registry policy](./registry.mdx) to enforce a specific registry URL +- [Non-registry servers policy](./non-registry-servers.mdx) to block servers + outside the registry +- [Degraded mode](../degraded-mode.mdx) to define client behavior when the + Enterprise Manager is unreachable diff --git a/docs/platform/enterprise-manager/policies/ca-certificate.mdx b/docs/platform/enterprise-manager/policies/ca-certificate.mdx new file mode 100644 index 00000000..454fa980 --- /dev/null +++ b/docs/platform/enterprise-manager/policies/ca-certificate.mdx @@ -0,0 +1,69 @@ +--- +title: CA certificate policy +description: + Inject a custom CA certificate into all MCP server containers so they can + reach internal services secured by a private certificate authority. +--- + +Use this directive when your MCP server containers need to reach internal +services (private registries, proxies, or APIs) that are secured by a +certificate authority not trusted by default. Injecting your corporate CA +certificate ensures containers can verify TLS connections without disabling +certificate verification. + +You'll need either the PEM-encoded certificate or a URL from which it can be +fetched, and the Enterprise Manager already [deployed](../configure.mdx) and +reachable by clients. + +## Configure the CA certificate directive + +Add the `ca_certificate` directive to your enterprise configuration. Supply +either `pem` (inline certificate) or `url` (fetched at runtime), but not both. + +### Inline PEM + +Use this when you want to embed the certificate directly in your configuration: + +```yaml title="values.yaml" +enterpriseConfig: + ca_certificate: + value: + pem: | + -----BEGIN CERTIFICATE----- + + -----END CERTIFICATE----- + enforcement: 'enforced' +``` + +### URL + +Use this when your CA certificate is hosted at a stable internal URL and you +want to avoid embedding it in the configuration: + +```yaml title="values.yaml" +enterpriseConfig: + ca_certificate: + value: + url: 'https://pki.example.com/ca.pem' + enforcement: 'enforced' +``` + +:::note + +`url` is fetched by the ToolHive client at startup, so it must be reachable from +the machines running ToolHive, not just from inside the MCP containers. + +::: + +After updating your configuration, +[apply the change](./index.mdx#apply-policy-changes). + +## Next steps + +- [Build environment policy](./build-env.mdx) to inject proxy settings or + internal endpoints into MCP containers +- [Registry policy](./registry.mdx) to enforce a specific registry URL +- [Non-registry servers policy](./non-registry-servers.mdx) to block servers + outside the registry +- [Degraded mode](../degraded-mode.mdx) to define client behavior when the + Enterprise Manager is unreachable diff --git a/docs/platform/enterprise-manager/policies/desktop-app.mdx b/docs/platform/enterprise-manager/policies/desktop-app.mdx new file mode 100644 index 00000000..28c3d66e --- /dev/null +++ b/docs/platform/enterprise-manager/policies/desktop-app.mdx @@ -0,0 +1,110 @@ +--- +title: Stacklok Desktop policies +description: + Control the visibility of the Playground tab and help menu in Stacklok + Desktop. +--- + +These directives control which features are visible in Stacklok Desktop. Use +them to hide elements that don't apply to your organization's workflow or to +enforce a consistent interface for all users. + +## Playground + +Use this directive to show or hide the +[Playground](../../../toolhive/guides-ui/playground.mdx) tab in Stacklok +Desktop. + +```yaml title="values.yaml" +enterpriseConfig: + playground: + # false = hide the Playground tab + # true = show the Playground tab + value: false + # "enforced" blocks local overrides; "default" lets users override locally + enforcement: 'enforced' +``` + +The combined behavior of the `value` and `enforcement` fields: + +| Enforcement | Value | Client behavior | +| ----------- | ------- | ---------------------------------------------------------------- | +| `enforced` | `false` | Clients must hide the Playground tab. Users cannot enable it. | +| `enforced` | `true` | Clients must show the Playground tab. Users cannot disable it. | +| `default` | `false` | Clients hide the Playground by default but users may enable it. | +| `default` | `true` | Clients show the Playground by default but users may disable it. | + +After updating your configuration, +[apply the change](./index.mdx#apply-policy-changes). + +### Variations + +#### Advisory hide + +Hide the Playground by default while allowing users to re-enable it locally: + +```yaml title="values.yaml" +enterpriseConfig: + playground: + value: false + enforcement: 'default' +``` + +#### Force Playground visible + +For sandbox or training environments where you want to guarantee the Playground +is accessible to all users: + +```yaml title="values.yaml" +enterpriseConfig: + playground: + value: true + enforcement: 'enforced' +``` + +## Help menu + +Use this directive to show or hide the help menu (**?** button) in Stacklok +Desktop. + +```yaml title="values.yaml" +enterpriseConfig: + help_menu: + # false = hide the help menu + # true = show the help menu + value: false + # "enforced" blocks local overrides; "default" lets users override locally + enforcement: 'enforced' +``` + +| Enforcement | Value | Client behavior | +| ----------- | ------- | --------------------------------------------------------------- | +| `enforced` | `false` | Clients must hide the help menu. Users cannot enable it. | +| `enforced` | `true` | Clients must show the help menu. Users cannot disable it. | +| `default` | `false` | Clients hide the help menu by default but users may enable it. | +| `default` | `true` | Clients show the help menu by default but users may disable it. | + +After updating your configuration, +[apply the change](./index.mdx#apply-policy-changes). + +### Variations + +#### Advisory hide + +Hide the help menu by default while allowing users to re-enable it locally: + +```yaml title="values.yaml" +enterpriseConfig: + help_menu: + value: false + enforcement: 'default' +``` + +## Next steps + +- [Registry policy](./registry.mdx) to enforce a specific registry URL +- [Non-registry servers policy](./non-registry-servers.mdx) to block servers + outside the registry +- [Telemetry policy](./telemetry.mdx) to enforce OpenTelemetry settings +- [Degraded mode](../degraded-mode.mdx) to define client behavior when the + Enterprise Manager is unreachable diff --git a/docs/platform/enterprise-manager/policies/index.mdx b/docs/platform/enterprise-manager/policies/index.mdx new file mode 100644 index 00000000..5cb89615 --- /dev/null +++ b/docs/platform/enterprise-manager/policies/index.mdx @@ -0,0 +1,70 @@ +--- +title: Policies +description: + How Enterprise Manager policies work, how to choose enforcement levels, and + how to apply policy changes across Stacklok clients. +--- + +Policies are the directives the Enterprise Manager pushes to Stacklok clients. +Each directive controls one aspect of client behavior: which MCP registry +clients connect to, whether non-registry servers are allowed, where telemetry +flows, and what parts of the Stacklok Desktop are visible. + +## Available directives + +| Directive | Use it to | +| --------------------------------------------------------------------- | ------------------------------------------------------- | +| [Registry](./registry.mdx) | Enforce a specific MCP registry URL | +| [Non-registry servers](./non-registry-servers.mdx) | Block or allow MCP servers that are not in the registry | +| [Telemetry](./telemetry.mdx) | Standardize OpenTelemetry collector configuration | +| [CA certificate](./ca-certificate.mdx) | Inject a custom CA certificate into MCP containers | +| [Build environment](./build-env.mdx) | Inject environment variables into MCP containers | +| [Stacklok Desktop](./desktop-app.mdx) | Show or hide the Playground tab and help menu | +| [AI assistant](../../enterprise-cloud-ui/configure.mdx#feature-flags) | Show or hide the AI assistant in the Cloud UI | + +Advanced directives — such as LLM Gateway configuration — are not covered in +these guides. + +## Enforcement levels + +Every policy directive carries an `enforcement` field with one of two values: + +| Enforcement | Meaning | +| ----------- | ------------------------------------------------------------------------------------ | +| `enforced` | Mandatory. Clients must use the configured value and cannot override it locally. | +| `default` | Advisory. Clients use the configured value as a default but may override it locally. | + +Use `enforced` when a policy needs to hold firm across the organization, for +example in regulated environments or when compliance requires it. Use `default` +when you want to recommend a configuration while still letting individual teams +or developers adjust for local needs. + +## Apply policy changes + +After updating `enterprise-manager.enterpriseConfig` in your Helm values, +upgrade the release to push the change to clients: + +```bash +helm upgrade stacklok-enterprise \ + oci://oci.stacklok.com/stacklok-enterprise//stacklok-enterprise-platform \ + --version \ + --namespace stacklok-system \ + --values values.yaml +``` + +Clients receive the updated policy the next time they connect to the Enterprise +Manager. + +## Next steps + +Pick a directive to configure: + +- [Registry policy](./registry.mdx) +- [Non-registry servers policy](./non-registry-servers.mdx) +- [Telemetry policy](./telemetry.mdx) +- [CA certificate policy](./ca-certificate.mdx) +- [Build environment policy](./build-env.mdx) +- [Stacklok Desktop policies](./desktop-app.mdx) + +Or read about [degraded mode](../degraded-mode.mdx) to control how clients +behave when the Enterprise Manager is unreachable. diff --git a/docs/platform/enterprise-manager/policies/non-registry-servers.mdx b/docs/platform/enterprise-manager/policies/non-registry-servers.mdx new file mode 100644 index 00000000..d39fd701 --- /dev/null +++ b/docs/platform/enterprise-manager/policies/non-registry-servers.mdx @@ -0,0 +1,82 @@ +--- +title: Non-registry servers policy +description: + Block or allow MCP servers that are not listed in the configured registry. +--- + +A registry policy tells clients where to find approved servers, but without a +non-registry servers policy, developers can still run unapproved servers by +adding them locally. This guide shows you how to close that gap. + +Pair this guide with a [Registry policy](./registry.mdx) so clients have a +single approved registry to pull from. + +## Configure the non-registry servers directive + +Add the `non_registry_servers` directive to your enterprise configuration. The +`value` field controls whether non-registry servers are allowed (`true`) or +blocked (`false`); pick an [enforcement level](./index.mdx#enforcement-levels) +to match. + +```yaml title="values.yaml" +enterpriseConfig: + non_registry_servers: + # false = only registry servers are allowed (recommended for most orgs) + # true = any server is allowed + value: false + # "enforced" blocks local overrides; "default" lets users override locally + enforcement: 'enforced' +``` + +The combined behavior of the `value` and `enforcement` fields: + +| Enforcement | Value | Client behavior | +| ----------- | ------- | ---------------------------------------------------------------- | +| `enforced` | `false` | Clients cannot run servers outside the registry. | +| `enforced` | `true` | Clients can run any server, even outside the registry. | +| `default` | `false` | Clients default to registry-only but may override locally. | +| `default` | `true` | Clients default to allowing any server and may override locally. | + +Use `enforced` with `value: false` in security-sensitive environments where +unreviewed code execution is not acceptable. Use `default` when you want to +nudge developers toward the registry catalog without hard-blocking local +experimentation. + +After updating your configuration, +[apply the change](./index.mdx#apply-policy-changes). + +## Variations + +### Advisory block + +Suggest registry-only servers as the org default while allowing developers to +run non-registry servers locally when needed: + +```yaml title="values.yaml" +enterpriseConfig: + non_registry_servers: + value: false + enforcement: 'default' +``` + +### Explicitly allow any server + +For sandbox or developer environments where you want to formally permit all +servers, set `value: true`. The `enforced` level prevents the policy from being +tightened locally: + +```yaml title="values.yaml" +enterpriseConfig: + non_registry_servers: + value: true + enforcement: 'enforced' +``` + +## Next steps + +- [Registry policy](./registry.mdx) to enforce a specific registry URL +- [Telemetry policy](./telemetry.mdx) to enforce OpenTelemetry settings +- [Stacklok Desktop policies](./desktop-app.mdx) to control Stacklok Desktop + visibility +- [Degraded mode](../degraded-mode.mdx) to define client behavior when the + Enterprise Manager is unreachable diff --git a/docs/platform/enterprise-manager/policies/registry.mdx b/docs/platform/enterprise-manager/policies/registry.mdx new file mode 100644 index 00000000..e6631c0d --- /dev/null +++ b/docs/platform/enterprise-manager/policies/registry.mdx @@ -0,0 +1,78 @@ +--- +title: Registry policy +description: Enforce a specific MCP registry URL for all Stacklok clients. +--- + +Without a registry policy, developers can point ToolHive to any MCP registry, +including unapproved ones. This guide shows you how to lock all clients to your +internal registry so developers always pull from your vetted server catalog. The +registry can be a self-hosted +[Registry Server](../../../toolhive/guides-registry/index.mdx), the upstream MCP +registry, or any MCP-compatible registry. + +You'll need your registry's URL (for example, `https://registry.acme.com`) and +the Enterprise Manager already [deployed](../configure.mdx) and reachable by +clients. + +## Configure the registry directive + +Add the `registry` directive to your enterprise configuration. Replace the +example URL with your registry's address and pick an +[enforcement level](./index.mdx#enforcement-levels). + +```yaml title="values.yaml" +enterpriseConfig: + registry: + value: + # The registry API URL all clients connect to + api_url: 'https://registry.acme.com' + # "enforced" blocks local overrides; "default" lets users override locally + enforcement: 'enforced' +``` + +Use `enforced` in regulated environments or when you need to guarantee that only +vetted servers are accessible. Use `default` when you want to recommend a +registry URL across your organization but allow teams or developers to switch +for testing or local development. + +After updating your configuration, +[apply the change](./index.mdx#apply-policy-changes). + +## Variations + +### Advisory registry + +Suggest the registry URL as an org-wide default while allowing local overrides: + +```yaml title="values.yaml" +enterpriseConfig: + registry: + value: + api_url: 'https://registry.acme.com' + enforcement: 'default' +``` + +### Registry on a private IP + +If your registry responds with private IP addresses (for example, a registry +that runs inside your VPC), set `allow_private_ip: true` so clients accept those +responses: + +```yaml title="values.yaml" +enterpriseConfig: + registry: + value: + api_url: 'https://registry.internal.acme.com' + allow_private_ip: true + enforcement: 'enforced' +``` + +## Next steps + +- [Non-registry servers policy](./non-registry-servers.mdx) to control whether + clients can run servers outside the registry +- [Telemetry policy](./telemetry.mdx) to enforce OpenTelemetry settings +- [Stacklok Desktop policies](./desktop-app.mdx) to control Stacklok Desktop + visibility +- [Degraded mode](../degraded-mode.mdx) to define client behavior when the + Enterprise Manager is unreachable diff --git a/docs/platform/enterprise-manager/policies/telemetry.mdx b/docs/platform/enterprise-manager/policies/telemetry.mdx new file mode 100644 index 00000000..977e5586 --- /dev/null +++ b/docs/platform/enterprise-manager/policies/telemetry.mdx @@ -0,0 +1,100 @@ +--- +title: Telemetry policy +description: Enforce OpenTelemetry settings across Stacklok clients. +--- + +This guide walks you through configuring a telemetry policy that routes all +client traces and metrics to your centralized OpenTelemetry collector, +regardless of any local configuration developers may have set. For a primer on +ToolHive's OpenTelemetry support, see the +[OpenTelemetry integration](../../../toolhive/integrations/opentelemetry.mdx) +guide. + +You'll need the OTLP HTTP endpoint for your collector (for example, +`https://otel.acme.com`) and the Enterprise Manager already +[deployed](../configure.mdx) and reachable by clients. + +## Configure the telemetry directive + +Add the `telemetry` directive to your enterprise configuration. Replace the +example endpoint and headers with your collector's settings and pick an +[enforcement level](./index.mdx#enforcement-levels). + +```yaml title="values.yaml" +enterpriseConfig: + telemetry: + value: + # Required: OTLP HTTP endpoint for your OpenTelemetry collector + otel_endpoint: 'https://otel.acme.com' + # Fraction of traces to sample: 0.0 = none, 1.0 = all + sampling_rate: 0.1 + tracing_enabled: true + metrics_enabled: true + # Non-sensitive headers only; do not include API keys or credentials + headers: + x-tenant-id: 'acme-prod' + # "enforced" blocks local overrides; "default" lets users override locally + enforcement: 'enforced' +``` + +Use `enforced` when your organization requires all telemetry to flow to a +central collector, for example for compliance, cost control, or security +monitoring. Use `default` when you want to push recommended OpenTelemetry +settings to developers but allow teams to route telemetry to their own +collectors for local debugging or testing. + +:::warning + +The `headers` field is for non-sensitive metadata only (tenant ID, environment +name, and so on). Do not include API keys, tokens, or other credentials here. +Use your identity provider and OIDC token exchange for authenticated collector +access. + +::: + +After updating your configuration, +[apply the change](./index.mdx#apply-policy-changes). + +## Variations + +### Advisory telemetry settings + +Push OpenTelemetry settings as org-wide defaults while allowing teams to +override them: + +```yaml title="values.yaml" +enterpriseConfig: + telemetry: + value: + otel_endpoint: 'https://otel.acme.com' + sampling_rate: 0.1 + tracing_enabled: true + metrics_enabled: true + enforcement: 'default' +``` + +### Plain-text (insecure) collector endpoint + +For internal collectors that don't use TLS, for example on a private network, +set `insecure: true`: + +```yaml title="values.yaml" +enterpriseConfig: + telemetry: + value: + otel_endpoint: 'http://otel.internal.acme.com:4318' + insecure: true + tracing_enabled: true + metrics_enabled: true + enforcement: 'enforced' +``` + +## Next steps + +- [Registry policy](./registry.mdx) to enforce a specific registry URL +- [Non-registry servers policy](./non-registry-servers.mdx) to block servers + outside the registry +- [Stacklok Desktop policies](./desktop-app.mdx) to control Stacklok Desktop + visibility +- [Degraded mode](../degraded-mode.mdx) to define client behavior when the + Enterprise Manager is unreachable diff --git a/docs/platform/enterprise-platform/airgap-install.mdx b/docs/platform/enterprise-platform/airgap-install.mdx new file mode 100644 index 00000000..e565c820 --- /dev/null +++ b/docs/platform/enterprise-platform/airgap-install.mdx @@ -0,0 +1,633 @@ +--- +title: Install from a private registry (air-gapped) +description: + Mirror the Stacklok Enterprise chart and images from Replicated into your own + container registry, then install the platform from there. +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +The [standard deployment](./deployment.mdx) pulls the umbrella Helm chart and +its images from Replicated at install time, using the license credentials you +receive during onboarding. That works when your cluster can reach Replicated +while it installs. When it can't, or your security posture requires every +artifact to come from an internal registry, use this air-gapped path instead. It +produces the same umbrella release, just sourced from your own registry. + +## How it works + +You move the platform artifacts across the air gap once, then install from your +side of it: + +1. **Pull the bundle from Replicated.** Use the Stacklok install portal's Helm + air-gap flow to download the umbrella chart and the container images it + references. +1. **Mirror into your registry.** Push the chart and images into a private OCI + registry you control. Amazon Elastic Container Registry (ECR) is the worked + example below, but any OCI-compliant registry works the same way. +1. **Repoint the install.** Override the chart's image references and Helm + source to your registry, and supply a pull secret. +1. **Install with Helm.** Run `helm install` against your registry and verify + the platform comes up. + +```mermaid +flowchart LR + Replicated["Stacklok install portal\n(Replicated proxy registry)"] + Station["Transfer workstation\n(skopeo + helm)"] + Registry["Your private\nOCI registry"] + Cluster["Your Kubernetes\ncluster"] + + Replicated -->|"pull chart + images\n(License ID auth)"| Station + Station -->|"skopeo copy --all\nhelm push"| Registry + Registry -->|"helm install\n(chart + image pulls)"| Cluster +``` + +Upgrades follow the same path: when Stacklok publishes a new release, re-pull +the chart and images at the new version, mirror them into your registry again, +then re-run the install (or let your GitOps controller reconcile the bumped +version). See [Automate with GitOps](#automate-with-gitops). + +## Prerequisites + +Before you start, make sure you have: + +- The platform prerequisites from the + [standard deployment](./deployment.mdx#prerequisites): a Kubernetes cluster + (1.28 or later), an ingress controller, an OIDC-compatible identity provider + configured per [Configure platform identity](./configure-identity.mdx), and a + PostgreSQL database for the Registry Server (the MCP and skills catalog + component, distinct from the private OCI image registry this page mirrors + into). +- A **StorageClass** for persistent volumes (only needed if you're enabling the + AI Gateway). Valkey runs as a StatefulSet with a `ReadWriteOnce` persistent + volume claim, so the cluster needs a StorageClass, ideally one marked + **default**. If none is marked default, the claim stays `Pending` and Valkey + never starts; set the class explicitly in your values (see + [Step 4](#step-4-point-the-install-at-your-registry)). +- A private OCI registry you control (Amazon ECR, Google Artifact Registry, + Azure Container Registry, Artifactory, Harbor, or any OCI-compliant registry) + that your cluster can pull from. +- Your Stacklok Enterprise license, available from the Stacklok install portal + at [install.stacklok.com](https://install.stacklok.com). Helm and your image + tooling authenticate to the Replicated proxy registry using your **License + ID** as the password. Stacklok sends portal access instructions during + onboarding. +- A transfer workstation that can reach both the Replicated proxy registry and + your own registry, with these tools installed: + - A tool that can mirror multi-arch images between registries, such as + [`skopeo`](https://github.com/containers/skopeo) or + [`crane`](https://github.com/google/go-containerregistry). The examples + below use `skopeo`. + - [`helm`](https://helm.sh/) 3.8 or later (the version that supports OCI + registries) + - `kubectl` to install and verify + - Your registry's CLI for authentication (for example, the AWS CLI v2 for ECR) + +:::note[Egress for the transfer workstation] + +The transfer workstation needs outbound HTTPS (port 443) to the Stacklok +distribution hosts and to your own registry. In a locked-down network, get these +allowlisted first; a blocked host surfaces as an opaque TLS or connection +timeout rather than a clear error: + +- the install portal (`install.stacklok.com`) +- the OCI chart host and image-proxy host the portal prints for your release + (typically an `oci.*` host and an `image-proxy.*` host) +- `proxy.replicated.com`, if the Replicated SDK is enabled +- your own registry host, so you can push + +The cluster's own egress (identity provider issuer, telemetry, upstream MCP +endpoints) is separate and isn't covered here. + +::: + +:::note[Fully disconnected environments] + +If the transfer workstation can't reach your registry and your cluster at the +same time, the install portal also produces a downloadable air-gap bundle (a +`.tgz` of the chart and images). Move the bundle across the gap on your own +media, then run the mirror steps below from a workstation on the internal +network. The commands are identical once the artifacts are local. + +When you unpack the bundle, watch the OCI layout. A `tar --strip-components` can +drop the `index.json` at the layout root, and archives created on macOS carry +`__MACOSX/._*` junk that confuses tooling. Run `find . -name oci-layout` to +confirm the layout root after extraction, and test the exact unpack commands on +your target operating system. + +::: + +## Step 1: Get your license and image list + +Stacklok distributes the platform through Replicated and gives you a +customer-facing install portal at +[install.stacklok.com](https://install.stacklok.com). Log in with the +credentials Stacklok provides during onboarding. The portal hosts your license, +the chart, and per-release install instructions. + +In the portal, open the **Existing cluster with Helm** instructions and select +the air-gap flow. The portal generates the exact commands for your release, +authenticated with your license. The flow covers: + +- Authenticating Helm and your image tooling to the Replicated proxy registry, + using your **License ID** as the password +- Listing the container images the chart references for your release +- Pulling the umbrella chart and those images + +Note your **License ID** and the image references the portal lists. You use the +License ID to authenticate in [Step 3](#step-3-mirror-the-chart-and-images), and +the image references tell you what to mirror. + +:::warning[Mirror every image the portal lists] + +Use the portal's image list for your release as the source of truth, and mirror +every image on it. Don't try to derive the list yourself by rendering the chart: +the platform subcharts are disabled by default, so a plain `helm template` +doesn't render most of them; some images are pinned inside subcharts or passed +through as operator settings as full `repo:tag` strings; and a few default to +other registries such as `docker.io` or `mcr.microsoft.com`. Grepping +`values.yaml` for `image.repository` fields under-reports for the same reasons. +If anything looks missing for your release, confirm the full set with Stacklok +before you install. An image you miss surfaces later as an `ImagePullBackOff`, +because the cluster pulls only from your registry and the missing image was +never mirrored into it. + +::: + +## Step 2: Create repositories in your registry + +Create one repository per artifact you're mirroring: one for the umbrella chart +and one for each container image from Step 1. The repository layout under your +registry prefix should mirror the source paths so the image overrides in +[Step 4](#step-4-point-the-install-at-your-registry) stay simple. + + + + +ECR requires each repository to exist before you can push to it. Create them up +front. The example uses an `aws ecr create-repository` call per artifact: + +```bash +export AWS_REGION= +export ECR_PREFIX= # for example: stacklok-enterprise + +for repo in \ + stacklok-enterprise-platform \ + ; do + aws ecr create-repository \ + --region "$AWS_REGION" \ + --repository-name "$ECR_PREFIX/$repo" \ + --image-tag-mutability IMMUTABLE >/dev/null \ + && echo "created $ECR_PREFIX/$repo" +done +``` + +Replace ``, ``, and so on with the image names from the +portal's image list. List your repositories with +`aws ecr describe-repositories --region "$AWS_REGION"` to confirm. + + + + +Many registries (Harbor, Google Artifact Registry, Azure Container Registry) +create repositories automatically on first push, so you can skip ahead to +mirroring. If yours requires repositories to exist first, create one per +artifact from Step 1 (the umbrella chart plus each image) under a shared prefix, +using your registry's console or CLI. + +Whichever registry you use, note its host (for example, `myorg.jfrog.io` or +`-docker.pkg.dev/`) and a prefix to group the Stacklok +artifacts. You reference both in the next steps. + + + + +## Step 3: Mirror the chart and images + +Authenticate to both registries, then copy each artifact from Replicated into +your own. + +### Authenticate to both registries + +You authenticate to two registries: the Replicated proxy registry you pull +**from** (the source), and your own registry you push **to** (the target). + +Log in to the source with your **License ID** from +[Step 1](#step-1-get-your-license-and-image-list). Replicated serves the chart +and the images from different hosts: the chart from an `oci.*` host (Helm) and +the images from an `image-proxy.*` host (skopeo). Use the exact hosts the +install portal printed for your release; the logins follow this shape: + +```bash +# Use your License ID as the password for both. +# Images: skopeo pulls from the image-proxy host. +skopeo login \ + --username --password + +# Chart: helm pulls from the OCI chart host. +helm registry login \ + --username --password +``` + +Then log in to your target registry: + + + + +Log `skopeo` and `helm` in to ECR with a short-lived token: + +```bash +export ECR_HOST=.dkr.ecr..amazonaws.com + +aws ecr get-login-password --region "$AWS_REGION" \ + | skopeo login --username AWS --password-stdin "$ECR_HOST" + +aws ecr get-login-password --region "$AWS_REGION" \ + | helm registry login --username AWS --password-stdin "$ECR_HOST" +``` + +ECR tokens from `get-login-password` expire after 12 hours, so run these again +if a long mirror session outlives the token. + + + + +Log `skopeo` and `helm` in to your registry with the credentials it issues: + +```bash +export TARGET_HOST= + +skopeo login "$TARGET_HOST" +helm registry login "$TARGET_HOST" +``` + +Use whatever credential your registry expects (a robot account, a personal +access token, or a service-account key). The rest of the flow is identical. + + + + +### Copy the images + +Mirror each image with `skopeo copy --all`. The `--all` flag copies the full +multi-arch manifest list (for example, `amd64` and `arm64`) rather than a single +platform, so the image still resolves on every node type in your cluster: + +```bash +skopeo copy --all \ + docker:///: \ + docker://$ECR_HOST/$ECR_PREFIX/: +``` + +Run one `skopeo copy` per image from the portal's image list. Take the +`` and `:` values directly from that list so +the tags match what the chart expects. The `docker://` prefix is skopeo's +registry transport, not a dependency on Docker; you don't need a container +engine for this step. + +:::note[Preserve the multi-arch manifest] + +[`crane copy`](https://github.com/google/go-containerregistry) preserves the +manifest list the same way and is a drop-in alternative (it takes bare image +references, with no `docker://` prefix). Avoid mirroring with a container +engine's single-image pull and push, such as `docker pull` and `docker push` (or +the `podman` and `nerdctl` equivalents), which flattens the image to one +architecture and breaks pulls on mixed `amd64` and `arm64` clusters. + +::: + +### Push the chart + +Pull the umbrella chart from Replicated (the portal's generated command does +this), then push it to your registry as an OCI artifact: + +```bash +helm push \ + stacklok-enterprise-platform-.tgz \ + oci://$ECR_HOST/$ECR_PREFIX +``` + +Verify the chart landed by pulling it back: + +```bash +helm pull oci://$ECR_HOST/$ECR_PREFIX/stacklok-enterprise-platform \ + --version +``` + +Confirm the version that resolves is the one you pushed, not a stale cached +entry under a reused tag (a stale chart surfaces later as confusing CRD-schema +mismatches): + +```bash +helm show chart oci://$ECR_HOST/$ECR_PREFIX/stacklok-enterprise-platform \ + --version | grep '^version:' +``` + +## Step 4: Point the install at your registry + +The umbrella chart's image references default to the Replicated-hosted registry +it ships from. Override them so every pull comes from your registry, and give +the chart a pull secret for it. + +### Create an image pull secret + +Create a `docker-registry` secret in the install namespace so the cluster can +authenticate when it pulls images. Despite the name, this Kubernetes secret type +works for any OCI registry, not just Docker Hub; it's how the kubelet stores +registry pull credentials. + + + + +```bash +kubectl create namespace stacklok-system --dry-run=client -o yaml \ + | kubectl apply -f - + +kubectl create secret docker-registry stacklok-enterprise-pull \ + --namespace stacklok-system \ + --docker-server="$ECR_HOST" \ + --docker-username=AWS \ + --docker-password="$(aws ecr get-login-password --region "$AWS_REGION")" +``` + +An ECR pull token expires after 12 hours, so don't pin it into a static secret. +Use a refreshing credential instead. See +[Keep registry credentials fresh](#keep-registry-credentials-fresh). + + + + +```bash +kubectl create namespace stacklok-system --dry-run=client -o yaml \ + | kubectl apply -f - + +kubectl create secret docker-registry stacklok-enterprise-pull \ + --namespace stacklok-system \ + --docker-server="$TARGET_HOST" \ + --docker-username= \ + --docker-password= +``` + +If your registry issues long-lived robot credentials, this secret is all the +cluster needs. If it issues short-lived tokens, see +[Keep registry credentials fresh](#keep-registry-credentials-fresh). + + + + +### Override the image source in values + +Add your registry overrides and pull secret to the `values.yaml` from the +[standard deployment](./deployment.mdx#3-configure-values), so the chart pulls +every image from your registry instead of the default. + +This chart has no global image-registry override (no `global.imageRegistry`), +and no single `global.imagePullSecrets` that reaches every component. Repoint +each component's image, and set its pull secret, in that component's own +subchart values. The example below uses verified key paths; confirm them against +the `values.yaml` in the chart you pulled in Step 1, since paths can change +between versions. + +```yaml title="values.yaml (air-gap additions)" +# The air-gapped path uses your own pull secret, not the Replicated SDK. The +# standard values enable it; turn it off here, or it tries to pull from and +# phone home to Replicated. +replicated: + enabled: false + +# Cloud UI and Enterprise Manager: a structured image block plus their own +# imagePullSecrets list. +toolhive-cloud-ui: + image: + repository: //cloud-ui + tag: '' + imagePullSecrets: + - name: stacklok-enterprise-pull +enterprise-manager: + image: + repository: //toolhive-enterprise + tag: '' + imagePullSecrets: + - name: stacklok-enterprise-pull + +# Registry Server: image referenced as a full repo:tag string (not a +# repository/tag pair). +toolhive-registry-server: + upstream: + image: + registryServerUrl: //registry-api: + imagePullSecrets: + - name: stacklok-enterprise-pull + +# ToolHive operator: four images set as full repo:tag strings. The operator +# stamps the runner, vMCP, and registry-api refs onto the workloads it spawns, +# so nothing global could rewrite them. defaultImagePullSecrets propagates the +# pull secret to those spawned pods. +toolhive-operator: + upstream: + operator: + image: //operator: + toolhiveRunnerImage: //proxyrunner: + vmcpImage: //vmcp: + imagePullSecrets: + - name: stacklok-enterprise-pull + defaultImagePullSecrets: + - name: stacklok-enterprise-pull + registryAPI: + image: //registry-api: + +# AI Gateway operator (aliased enterprise-ai-gateway-operator). Its subchart +# images live under upstream.*, and it honors a subchart-scoped +# global.imagePullSecrets. Repoint the operator, AI Gateway controller and +# ext-proc, Envoy Gateway, ratelimit, Valkey, and Presidio image references +# here, following the same paths in the chart's values.yaml. +enterprise-ai-gateway-operator: + upstream: + global: + imagePullSecrets: + - name: stacklok-enterprise-pull +``` + +:::warning[Presidio is pinned by digest] + +Presidio is the only image the chart pins by `sha256` digest; the rest resolve +by tag. If you mirror with a tool that preserves the manifest digest (such as +`skopeo copy --all`), the pin still resolves from your registry. But if your +registry rewrites the manifest on push (some do), the digest no longer matches +and the pull fails even though the image is present. If you hit a Presidio +digest-mismatch error, clear the pin so it resolves by tag: + +```yaml +enterprise-ai-gateway-operator: + upstream: + presidio: + image: + digest: '' +``` + +::: + +If your cluster has no **default** StorageClass, pin one for Valkey. It's +deployed as a StatefulSet with a `ReadWriteOnce` claim, so without a class the +claim stays `Pending` and Valkey never starts: + +```yaml +enterprise-ai-gateway-operator: + upstream: + valkey: + persistence: + storageClass: # for example, an EBS gp3 class +``` + +Persistence is enabled by default with a 1Gi claim, so you only need to set the +class. + +## Step 5: Install and verify + +Install the chart from your registry with the merged values. Reference the chart +by its `oci://` URL rather than a Helm repo alias: + +```bash +helm install stacklok-enterprise \ + oci://$ECR_HOST/$ECR_PREFIX/stacklok-enterprise-platform \ + --version \ + --namespace stacklok-system \ + --create-namespace \ + --values values.yaml +``` + +Verify the platform comes up the same way as in the +[standard deployment](./deployment.mdx#5-verify-the-install): confirm every pod +in `stacklok-system` reaches `Running` and the ToolHive CRDs registered. + +In an air-gapped install, the failure mode to watch for is a pod stuck in +`ImagePullBackOff`. It almost always means an image the chart references wasn't +mirrored, or the pull secret can't authenticate. Recheck the pod's image against +the portal's image list to confirm you mirrored it, and confirm the +`stacklok-enterprise-pull` secret is valid. + +## Step 6: Prepare workload namespaces + +If you run MCP server and vMCP workloads in a namespace other than +`stacklok-system`, that namespace also needs pull access to your registry. The +operator stamps the `stacklok-enterprise-pull` secret onto every workload pod it +spawns (the `defaultImagePullSecrets` from +[Step 4](#step-4-point-the-install-at-your-registry)), and the kubelet resolves +it in the pod's own namespace. Provide that access the same way as +[Keep registry credentials fresh](#keep-registry-credentials-fresh): + +- **Long-lived credentials** (Artifactory or Harbor robot accounts, a GAR or ACR + service-account key): create `stacklok-enterprise-pull` in each workload + namespace, as you did in `stacklok-system`. That's all the namespace needs: + + ```bash + kubectl create namespace + + kubectl create secret docker-registry stacklok-enterprise-pull \ + --namespace \ + --docker-server= \ + --docker-username= \ + --docker-password= + ``` + +- **Short-lived tokens** (Amazon ECR's expire after 12 hours): grant registry + read to the node identity instead. The kubelet then pulls in any namespace, so + no static secret is needed and you can drop the `defaultImagePullSecrets` + override. + +## Automate with GitOps + +The manual flow above is the clearest way to understand the air-gap path, but +most teams run it through their existing infrastructure-as-code and GitOps +tooling so it's repeatable and auditable. + +- **Provision with infrastructure-as-code, in the right order.** Use Terraform, + OpenTofu, or your preferred tool to create the registry repositories and the + image pull secret. They must exist before the controller first reconciles, or + the HelmRelease fails with `chart not found` or `secret not found` and you're + debugging a race. Provision the registry side first, then point the controller + at it. +- **Reconcile with GitOps, from a self-refreshing source.** Point Flux or Argo + CD at your registry's `oci://` chart URL and let the controller install and + upgrade the umbrella release. Rather than a static pull secret, let the + controller pull the chart with a workload identity (for Flux, a + `HelmRepository` with `provider: aws` and no `secretRef`); this is the + chart-pull half of + [Keep registry credentials fresh](#keep-registry-credentials-fresh). The + values you assemble in [Step 4](#step-4-point-the-install-at-your-registry) go + in your Git source of truth. +- **Mirror new releases, and keep Git in sync with what you mirrored.** Wrap the + [Step 3](#step-3-mirror-the-chart-and-images) `skopeo` and `helm push` + commands in a job that runs whenever Stacklok publishes a new release. In an + air-gapped setup, Git can drift from reality: an out-of-band mirror leaves the + cluster on a version Git never recorded. Pin the exact mirrored version and + commit the bump when you mirror. + +:::note[If you automate further] + +Flux image-automation semver matchers ignore pre-release tags by default. And +infrastructure-as-code that creates registry repositories and IAM roles in a +single apply often needs retries: a freshly created IAM principal isn't +immediately usable, and the apply can fail with `Invalid principal in policy`. + +::: + +### Keep registry credentials fresh + +Many registries issue short-lived pull tokens (ECR's expire after 12 hours). A +static pull secret built from one of those tokens stops working once the token +expires, and pulls start failing on chart upgrades and on any pod that schedules +onto a new node. + +For a long-lived cluster, replace the static secret with a credential that +refreshes itself. Two separate pull paths need this, and they authenticate +differently. + +**Image pulls happen at the node level.** The kubelet pulls container images +before a pod's identity exists, so it authenticates with the node's identity, +not a pod or service-account identity. Grant registry read to the node: + +- **Amazon ECR:** Attach `AmazonEC2ContainerRegistryReadOnly` to the node group + role, or use the ECR credential provider. Images then pull without a static + secret. +- **Google Artifact Registry and Azure Container Registry:** Grant reader access + (GAR) or the `AcrPull` role (ACR) to the node service account or kubelet + identity. + +Once the node identity has registry read, you can drop the static +`imagePullSecrets` override from your values. + +**Chart pulls happen at the pod level.** A GitOps controller (for example, +Flux's source-controller) pulls the chart over the Helm OCI client from its own +pod, so pod-level workload identity fits here: + +- **Amazon ECR:** Use + [EKS Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html) + or IAM roles for service accounts. For Flux, set `provider: aws` on the + `HelmRepository` and drop its `secretRef`. +- **Google Artifact Registry:** Use + [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation) + to bind the controller's service account to a Google service account with + reader access. +- **Azure Container Registry:** Use + [workload identity](https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview) + with an `AcrPull` role assignment. + +## Next steps + +- [Verify the distribution](./verify-artifacts.mdx) to confirm the signatures, + provenance, and SBOMs of the images you mirrored +- [Configure platform identity](./configure-identity.mdx) to wire your identity + provider to the platform components +- [Configure policies](../enterprise-manager/policies/) to control client + behavior across your organization +- [Browse the catalog](../enterprise-cloud-ui/browse-catalog.mdx) once the Cloud + UI is running + +## Related information + +- [Deploy the platform](./deployment.mdx) - the standard install, which pulls + from Replicated at install time +- [Configure the Registry Server](./configure-registry-server.mdx) - the catalog + the Cloud UI reads diff --git a/docs/platform/enterprise-platform/configure-identity.mdx b/docs/platform/enterprise-platform/configure-identity.mdx new file mode 100644 index 00000000..cafeaec8 --- /dev/null +++ b/docs/platform/enterprise-platform/configure-identity.mdx @@ -0,0 +1,255 @@ +--- +title: Configure platform identity +description: + Configure your identity provider so the Stacklok Enterprise platform + components can authenticate users and validate their requests. +--- + +Stacklok Enterprise authenticates every request through your identity provider. +When a developer signs in to the Stacklok CLI, Stacklok Desktop, or Enterprise +Cloud UI, the identity provider issues an access token that the platform +components validate before serving any data. This page walks through the values +each component expects from your identity provider and shows two supported ways +to model them, so that each component accepts the tokens your provider issues. + +This page covers the shared platform components: the Enterprise Manager, +Enterprise Cloud UI, and Registry Server. Individual MCP servers validate tokens +through their own per-server OIDC configuration, which you set up alongside each +server's policy in [enterprise authorization](../enterprise-authz/index.mdx). +The same identity provider issues both, so the claims you configure here also +feed the role bindings on your MCP servers. + +You should already have an OIDC-compatible identity provider (Okta, Entra ID, or +a generic OIDC provider) where you can create authorization servers, OAuth +client applications, custom scopes, and custom claims. Stacklok Enterprise does +not ship its own identity provider. + +## What each component expects + +Each component validates a different slice of the access token. They share two +requirements: the token is a signed JSON Web Token (JWT) so they can verify it +locally against the identity provider's published key set, and the issuer +matches the URL configured on the component. Beyond that, each component looks +at a different combination of audience, scope, and claims. + +| Component | What the component verifies | +| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Enterprise Manager](../enterprise-manager/configure.mdx) | Issuer, an audience that matches the configured value, and the configured scope is present | +| [Enterprise Cloud UI](../enterprise-cloud-ui/configure.mdx) | Issuer and an audience that matches the Cloud UI's OAuth client application | +| [Registry Server](../../toolhive/guides-registry/index.mdx) | Issuer, the role claim that maps the user to a platform role, and the group claim used for bindings | +| [MCP servers](../enterprise-authz/quickstart-entra.mdx) | Issuer and an audience that matches the server's `MCPOIDCConfig`, plus the role and group claims that [enterprise authorization](../enterprise-authz/index.mdx) policies bind against | + +The Stacklok CLI and Stacklok Desktop obtain tokens from the identity provider +using the Proof Key for Code Exchange (PKCE) flow and present them to each +component. The Enterprise Cloud UI authenticates browser users through the +authorization code flow on its server side. The components do not call the +identity provider's introspection endpoint, so the authorization server you +choose must issue signed JWTs rather than opaque tokens. + +## Pick a setup + +The components do not care whether their tokens come from the same authorization +server or from different ones. They care that the audience on the token matches +the audience configured on the component. Two setups satisfy that requirement. + +**One authorization server per component.** Recommended where your identity +provider supports it. Create one custom authorization server per platform +component, give each its own audience, and point each component at its own +issuer URL. Tokens issued for one component cannot be replayed against another, +because their issuer values differ. + +**One authorization server with multiple audiences.** Use this when your +identity provider plan caps you at a single custom authorization server. For +example, an Okta tenant on the free tier ships with one pre-provisioned +authorization server named `default` and does not let you create more. List +every component's audience on that one server. Each token still carries exactly +one audience, so a token minted for the Enterprise Manager is still rejected by +the Cloud UI and vice versa. + +Either setup produces JWTs the components accept. Pick based on what your +identity provider plan supports, and use the worked example below as a template +for either path. + +## Set up your identity provider (Okta example) + +This walkthrough uses Okta with the second setup: a single authorization server +(`default`) carrying every component's audience. To use the first setup instead, +repeat steps 1 through 4 once per custom authorization server you create, and +point each component at the matching issuer URL in step 6. + +### Step 1: Add audiences to the authorization server + +The Enterprise Manager and Cloud UI both reject tokens whose `aud` claim does +not match the value configured on the chart. Each component needs its audience +listed on the authorization server that issues its tokens. + +In the Okta admin console, go to **Security** > **API** > **Authorization +Servers** and open the `default` server. On the **Settings** tab, add the +audiences your deployment uses: + +```text title="Audiences" +enterprise-manager +cloud-ui +``` + +The exact strings here must match the `audience` value you set on each component +when you deploy it. Step 6 shows where that mapping goes in each component's +`values.yaml`. + +### Step 2: Add the scope the Enterprise Manager requires + +The Enterprise Manager checks the access token for a scope before it returns +configuration to the client. Without a matching scope on the token, every config +request returns `401 Unauthorized`. The Cloud UI and Registry Server do not +require this scope. + +On the **Scopes** tab, add a custom scope: + +| Field | Value | +| ------------- | --------------------------- | +| Name | `toolhive:config:read` | +| Description | Read Stacklok configuration | +| Default scope | No | + +### Step 3: Add the claims your components map to platform roles + +The Registry Server and the per-server MCP authorization policies both read +group and role claims from the token to map each user to a platform role. The +platform matches against the claim names, so those matter, but the values flow +through from your identity provider's user profile. Adjust the value expressions +to match how your Okta tenant carries group membership and role assignment. + +On the **Claims** tab, add the claims each component reads: + +| Name | Include in token type | Value | Value type | Include in | +| -------- | --------------------- | ------------- | ---------- | ---------- | +| `groups` | Access Token | `user.groups` | Expression | Any scope | +| `roles` | Access Token | `user.roles` | Expression | Any scope | +| `email` | Access Token | `user.email` | Expression | Any scope | + +These are the IdP side of the claim setup. For how the platform maps these claim +names to platform roles, and the ConfigMap that overrides them, see +[claim mapping in enterprise authorization](../enterprise-authz/intro.mdx#claim-mapping). + +### Step 4: Add a default access policy + +The Okta free tier ships the `default` authorization server without an access +policy, so client applications cannot request scopes against it until you add +one. Paid tiers usually have a permissive default policy already in place, so +this step is a no-op for them. + +On the **Access Policies** tab, add a default policy with one rule that permits +your OAuth clients to request the audiences and scope from steps 1 and 2. + +### Step 5: Create OAuth client applications + +Each Stacklok Enterprise component authenticates against a different OAuth +client. The Stacklok CLI and Stacklok Desktop share a single native (PKCE) +application that requests tokens for the Enterprise Manager audience. The Cloud +UI uses its own confidential (web) application because it runs an authorization +code flow on its server. + +| Application | Type | Audience requested | +| --------------------------------- | -------------------------- | -------------------- | +| Stacklok CLI and Stacklok Desktop | Native (PKCE, no secret) | `enterprise-manager` | +| Enterprise Cloud UI | Web (confidential, secret) | `cloud-ui` | + +Note the client IDs and (for the Cloud UI) the client secret for the next step. + +### Step 6: Wire the values into the component charts + +Each component reads its issuer and audience from its chart's `values.yaml`. The +values you wire in here must match the audiences and scope you configured in +steps 1 through 5, or the component will reject the tokens at runtime. + +For the [Enterprise Manager](../enterprise-manager/configure.mdx): + +```yaml title="enterprise-manager values.yaml" +idpConfig: + issuer: 'https://.okta.com/oauth2/default' + audience: 'enterprise-manager' + requiredScope: 'toolhive:config:read' + idpType: 'okta' +clientID: '' +``` + +For the [Enterprise Cloud UI](../enterprise-cloud-ui/configure.mdx): + +```yaml title="cloud-ui values.yaml" +oidc: + issuerUrl: 'https://.okta.com/oauth2/default' + clientId: '' + clientSecret: '' +``` + +Replace `` with your Okta subdomain and the `<*_CLIENT_ID>` placeholders +with the client IDs from step 5. + +When deploying with the umbrella chart, nest these values under the component's +top-level key (`enterprise-manager:` and `toolhive-cloud-ui:` respectively). See +[Deploy the platform](./deployment.mdx#3-configure-values) for the full nested +structure. + +## Verify a token + +Once a Stacklok client can sign in, you can confirm the tokens it receives carry +the right values before wiring them into a component. Decode the access token +and check the issuer, audience, and scope: + +```bash +echo "" | cut -d. -f2 | base64 -d 2>/dev/null | jq '.iss, .aud, .scp' +``` + +For a token that the Enterprise Manager would accept, you should see: + +```text +"https://.okta.com/oauth2/default" +"enterprise-manager" +["toolhive:config:read"] +``` + +If `aud` does not match the value you configured on the component, or `scp` does +not include `toolhive:config:read` for an Enterprise Manager request, the +component returns `401 Unauthorized`. + +## Next steps + +- [Configure the Enterprise Manager](../enterprise-manager/configure.mdx) with + the issuer, audience, and scope from this page wired into its chart +- [Configure the Enterprise Cloud UI](../enterprise-cloud-ui/configure.mdx) with + the matching OIDC values + +## Troubleshooting + +
+Tokens are opaque, not JWTs + +Okta's org authorization server (the one without `/oauth2/` in the +issuer URL) issues opaque tokens that require server-side introspection. +Stacklok Enterprise validates JWTs locally against the authorization server's +key set and does not call the introspection endpoint, so opaque tokens are not +usable. Use a custom authorization server (`/oauth2/default` or one you create) +so that access tokens are issued as JWTs. + +
+ +
+`aud` is an array, not a string + +Some identity providers put a single audience into an array +(`"aud": ["enterprise-manager"]`). The components accept both shapes. If +validation still fails, confirm the string inside the array matches the +configured audience exactly, including any prefix like `api://`. + +
+ +
+`scp` is missing + +Okta only includes the scope claim on access tokens when the OAuth client +requested it. Confirm the client's authorize request includes +`scope=openid+toolhive:config:read` (or the equivalent for your client library) +and that the access policy on the authorization server permits the scope for +that client. + +
diff --git a/docs/platform/enterprise-platform/configure-registry-server.mdx b/docs/platform/enterprise-platform/configure-registry-server.mdx new file mode 100644 index 00000000..cbc06f98 --- /dev/null +++ b/docs/platform/enterprise-platform/configure-registry-server.mdx @@ -0,0 +1,145 @@ +--- +title: Configure the Registry Server +description: + Configuration reference for the Registry Server component in the Stacklok + Enterprise platform chart. +--- + +The Registry Server serves the approved MCP server and skills catalog that the +Enterprise Cloud UI and Stacklok Desktop clients consume. It ships as a +hardened, license-gated build in the Stacklok Enterprise platform chart. + +:::tip[Deploy the platform first] + +Install the Registry Server with the [platform chart](./deployment.mdx), which +deploys it alongside the other components. To run it in its own cluster, or to +maintain a separate registry per environment, enable only this component as +described in [Distributed deployments](./distributed-deployments.mdx). + +::: + +The enterprise Registry Server uses the same configuration schema as the open +source [Registry Server guides](../../toolhive/guides-registry/index.mdx). Every +configuration concern (sources, registries, sync policies, database, +authentication, and authorization) is identical, so this page covers enabling +the component and points to the open source reference for the field-level +detail. + +## Prerequisites + +Before deploying, ensure you have: + +- A Kubernetes cluster (1.28 or later) +- An external PostgreSQL database (14 or later) that you provide, with an + application user, and optionally a separate migration user with + schema-modification privileges; the Registry Server stores its catalog there +- Stacklok Enterprise distribution access, which includes the Helm chart and + container image registry credentials, provided by Stacklok during onboarding + +## Configure values + +Enable the Registry Server with its `registryServer.enabled` flag, then set its +configuration under the `toolhive-registry-server` key. The chart wraps the open +source Registry Server chart under an `upstream` alias, so those values sit +under `toolhive-registry-server.upstream`. + +The `upstream.config` block is the open source Registry Server configuration +schema, rendered verbatim into a ConfigMap. A functioning server needs at least +one `sources` entry and one `registries` entry in addition to the `database` +connection. The skeleton below shows only the database wiring; see the +[open source reference](#configuration-reference) for the rest. + +### Create the database credential Secrets + +Supply database passwords from Secrets, never inline in the `config` block. +Create a Secret for the application user's password. The Registry Server runs +schema migrations on startup; if you use a separate, more-privileged migration +user, create a second Secret for it. Otherwise the server reuses the application +password for migrations and you can skip it. + +```bash +kubectl create secret generic registry-db-credentials \ + --from-literal=password='' \ + -n stacklok-system + +# Only if you use a separate migration user +kubectl create secret generic registry-db-migrator-credentials \ + --from-literal=password='' \ + -n stacklok-system +``` + +### Set the values file + +Enable the component, point it at your database, and reference the Secrets you +just created: + +```yaml title="values.yaml" +# Enable only the Registry Server. +registryServer: + enabled: true + +# Registry Server configuration. +toolhive-registry-server: + upstream: + config: + # sources and registries are required for a working server. See the + # configuration reference below. + database: + host: 'postgres.example.com' + port: 5432 + user: 'registry' # application user + migrationUser: 'registry_migrator' # elevated user for migrations + database: 'registry' + sslMode: 'require' + # Passwords from Secrets, keyed by the env vars the server reads. + extraEnv: + - name: THV_REGISTRY_DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: registry-db-credentials + key: password + # Only if you use a separate migration user + - name: THV_REGISTRY_DATABASE_MIGRATIONPASSWORD + valueFrom: + secretKeyRef: + name: registry-db-migrator-credentials + key: password +``` + +### Configuration reference + +The `upstream.config` block accepts every field the open source Registry Server +supports. See the open source reference for the detail: + +- [Configuration](../../toolhive/guides-registry/configuration.mdx) for sources, + registries, and sync policies +- [Database](../../toolhive/guides-registry/database.mdx) for connection + details, the migration user, and credential mechanisms +- [Authentication](../../toolhive/guides-registry/authentication.mdx) and + [Authorization](../../toolhive/guides-registry/authorization.mdx) for securing + the API +- [Telemetry and metrics](../../toolhive/guides-registry/telemetry-metrics.mdx) + for observability + +## Connect the Cloud UI + +Install the platform chart with these values as described in +[Deploy the platform](./deployment.mdx). Once running, verify the pod: + +```bash +kubectl get pods -n stacklok-system -l app.kubernetes.io/component=registry-api +``` + +The chart exposes the Registry Server through an in-cluster Service named +`registry-api` on port 8080. Point the Cloud UI at it with +`toolhive-cloud-ui.apiBaseUrl`, for example +`http://registry-api.stacklok-system.svc.cluster.local:8080`. + +## Next steps + +- [Deploy the platform](./deployment.mdx) to install the Registry Server + alongside the rest of the platform +- [Publish servers](../../toolhive/guides-registry/publish-servers.mdx) to + populate the catalog +- [Browse the catalog](../enterprise-cloud-ui/browse-catalog.mdx) in the Cloud + UI once the Registry Server is running diff --git a/docs/platform/enterprise-platform/deployment.mdx b/docs/platform/enterprise-platform/deployment.mdx new file mode 100644 index 00000000..85a16d57 --- /dev/null +++ b/docs/platform/enterprise-platform/deployment.mdx @@ -0,0 +1,335 @@ +--- +title: Deploy the platform +description: + Install the Stacklok Enterprise platform in your Kubernetes cluster as a + single Helm release. +--- + +The Stacklok Enterprise platform runs in your Kubernetes cluster. You install it +as a single umbrella Helm chart that deploys the ToolHive Operator, the +Enterprise Manager, the Enterprise Cloud UI, and the Registry Server in one +release, along with the custom resource definitions (CRDs) the operator needs. + +:::tip[Distributed and multi-cluster deployments] + +The umbrella chart can also run a subset of components, so you can spread the +platform across clusters or maintain separate registries per environment. See +[Distributed deployments](./distributed-deployments.mdx). + +::: + +:::tip[Air-gapped or strict-egress clusters] + +This page installs the chart directly from Replicated, which requires outbound +access at install time. If your cluster can't reach Replicated, or your security +posture requires every artifact to come from an internal registry, see +[Install from a private registry (air-gapped)](./airgap-install.mdx) instead. + +::: + +## Prerequisites + +Before deploying, ensure you have: + +- A Kubernetes cluster (1.28 or later) +- An ingress or gateway controller to publish the components, plus DNS records + and TLS certificates for the hostnames you expose the Cloud UI, Enterprise + Manager, and Registry Server on. The chart creates Services but no ingress, + DNS, or certificates; see [Step 6](#6-expose-the-platform-endpoints). +- An OIDC-compatible identity provider configured per + [Configure platform identity](./configure-identity.mdx) +- A PostgreSQL database for the [Registry Server](#what-the-chart-includes). The + Registry Server serves the MCP and skills catalog the Enterprise Cloud UI + reads, and stores it in an external PostgreSQL instance you provide. +- Your Stacklok Enterprise license, available from the Stacklok install portal + at [install.stacklok.com](https://install.stacklok.com). The license grants + access to the umbrella chart and the container images it references. Stacklok + sends portal access instructions during onboarding. + +## What the chart includes + +The chart bundles each platform component and gives it an enable flag in a +single `values.yaml`, so you turn on only the components you want. Each flag +maps to a platform component: + +| Enable flag | What it deploys | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `toolhiveOperator` | The ToolHive Operator and its custom resource definitions (`MCPServer`, `VirtualMCPServer`, and others) | +| `enterpriseManager` | Enterprise Manager service that serves configuration to Stacklok clients | +| `cloudUi` | Enterprise Cloud UI Next.js application | +| `registryServer` | Registry Server that serves the MCP and skills catalog to the Enterprise Cloud UI, backed by an external PostgreSQL database. | +| `aiGateway` | AI Gateway operator that exposes large language models behind a managed gateway. Disabled by default. | + +The `toolhiveOperator` subchart deploys the enterprise build of the ToolHive +Operator: the same operator codebase as ToolHive Community, repackaged as a +hardened, signed, and versioned image. It manages the same workload types as the +Community operator, so the existing guides apply unchanged. Use +[Run MCP servers in Kubernetes](../../toolhive/guides-k8s/run-mcp-k8s.mdx) for +MCP servers and remote proxies, and the +[Virtual MCP Server guides](../../toolhive/guides-vmcp/index.mdx) for Virtual +MCP Server (vMCP) gateways. + +:::note[Namespaces] + +This guide installs the platform into `stacklok-system` and uses that namespace +throughout. You can choose a different one; adjust the commands to match. + +The operator can manage MCP server workloads in any namespace. Running them in +their own namespace, separate from the platform components in `stacklok-system`, +keeps the platform and the workloads it manages apart, and is the recommended +setup. The Kubernetes guides linked above use `toolhive-system` in their +examples, so substitute your own namespaces as you follow them. + +::: + +## Deploy with Helm + +### 1. Authenticate to the Replicated registry + +Stacklok distributes the platform through Replicated. Your license, the umbrella +chart, and per-release install instructions all live in the install portal at +[install.stacklok.com](https://install.stacklok.com). Log in with the +credentials Stacklok provides during onboarding. + +The portal serves the chart from an OCI registry (`oci.stacklok.com`), not a +classic Helm chart repository, so you authenticate with `helm registry login` +rather than `helm repo add`. Use your license email as the username and your +**License ID** as the password: + +```bash +helm registry login oci.stacklok.com \ + --username \ + --password +``` + +In the portal, the **Existing cluster with Helm** instructions generate the +exact login and install commands for your release, including your channel slug +and the current chart version. Note those values; you reference them when you +[install the chart](#4-install-the-chart). + +:::note[Image pulls during install] + +You don't create an image pull secret for this online install. The chart's +Replicated integration provisions the pull credentials from your license, so the +cluster pulls component images at install time. The +[air-gapped path](./airgap-install.mdx) differs: you mirror the images and +create the pull secret yourself. + +::: + +### 2. Prepare secrets + +The chart expects a few Secrets to already exist. They live in the namespace the +platform installs into, so create that namespace first: + +```bash +kubectl create namespace stacklok-system +``` + +Then prepare the Secrets for the components you're enabling before you configure +values. + +**Enterprise Manager signing key.** Generate the key and create the Secret as +described in +[Generate a signing key](../enterprise-manager/configure.mdx#generate-a-signing-key). +The values file references it by name through +`enterprise-manager.signingConfig.existingSecret`. + +**Better Auth session secret.** The Cloud UI needs a secret of at least 32 +characters to encrypt its sessions. Generate one: + +```bash +openssl rand -base64 32 +``` + +Use the output as `toolhive-cloud-ui.betterAuth.secret` in the values file. + +**Registry Server database passwords.** Create a Secret for the database +password (and a second one if you use a separate migration user), as described +in +[Create the database credential Secrets](./configure-registry-server.mdx#create-the-database-credential-secrets). +The values file references them through `secretKeyRef`, as the example below +shows. + +### 3. Configure values + +Create a `values.yaml` file that enables the components you want and supplies +their settings. Each component has an enable flag, and its configuration goes +under that component's own key, as the example shows. The example below installs +the operator, Enterprise Manager, Cloud UI, and Registry Server, with the AI +Gateway disabled: + +```yaml title="values.yaml" +# Replicated SDK subchart. Required for the online install: it turns the license +# credentials injected at chart-pull time into the enterprise-pull-secret image +# pull secret the platform components reference. +replicated: + enabled: true + +# Enable only the components you want. +toolhiveOperator: + enabled: true +enterpriseManager: + enabled: true +cloudUi: + enabled: true +registryServer: + enabled: true +aiGateway: + enabled: false + +# Enterprise Manager configuration. See the Enterprise Manager deployment page +# for the full reference of fields under this key. +enterprise-manager: + idpConfig: + issuer: 'https://idp.example.com' + audience: 'enterprise-manager' + requiredScope: 'toolhive:config:read' + idpType: 'generic' + signingConfig: + # Secret created in "Prepare secrets" above + existingSecret: 'enterprise-manager-signing-key' + resourceURL: 'https://config.example.com' + clientID: '' + +# Registry Server configuration. Serves the MCP and skills catalog the Cloud +# UI reads. Requires an external PostgreSQL database; supply the password from +# a Secret, never inline. +toolhive-registry-server: + upstream: + config: + database: + host: 'postgres.example.com' + port: 5432 + user: 'thv_user' + database: 'toolhive_registry' + sslMode: 'require' + extraEnv: + - name: THV_REGISTRY_DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: registry-db-credentials + key: password + +# Enterprise Cloud UI configuration. See the Cloud UI deployment page for the +# full reference of fields under this key. +toolhive-cloud-ui: + # URL of the Registry Server. When it runs in this cluster, point at its + # in-cluster Service (named registry-api on port 8080). + apiBaseUrl: 'http://registry-api.stacklok-system.svc.cluster.local:8080' + # Cloud UI backend's URL for the Enterprise Manager; matches resourceURL + # above. + enterpriseManagerUrl: 'https://config.example.com' + oidc: + issuerUrl: 'https://idp.example.com' + clientId: '' + clientSecret: '' + betterAuth: + # Generated in "Prepare secrets" above (openssl rand -base64 32) + secret: '' + url: 'https://cloud-ui.example.com' +``` + +For the full reference of fields each component accepts, see +[Configure the Enterprise Manager](../enterprise-manager/configure.mdx), +[Configure the Enterprise Cloud UI](../enterprise-cloud-ui/configure.mdx), and +[Configure the Registry Server](./configure-registry-server.mdx). + +### 4. Install the chart + +Install the chart into the `stacklok-system` namespace you created earlier. +Reference the chart by its `oci://` URL, using the channel slug and version from +[Step 1](#1-authenticate-to-the-replicated-registry): + +```bash +helm install stacklok-enterprise \ + oci://oci.stacklok.com/stacklok-enterprise//stacklok-enterprise-platform \ + --version \ + --namespace stacklok-system \ + --values values.yaml +``` + +### 5. Verify the install + +Wait for the platform pods to reach the `Running` state: + +```bash +kubectl get pods -n stacklok-system +``` + +Confirm the ToolHive CRDs registered: + +```bash +kubectl get crd | grep toolhive.stacklok.dev +``` + +### 6. Expose the platform endpoints + +The chart creates ClusterIP Services for the components but no ingress. Publish +these three through your ingress or gateway controller so browsers and clients +outside the cluster can reach them at the hostnames you set in +[Step 3](#3-configure-values). List the Services to get their names (two are +prefixed with your Helm release name, `stacklok-enterprise`): + +```bash +kubectl get svc -n stacklok-system +``` + +Route each external hostname to its Service with the Ingress, HTTPRoute, or +Gateway resources your controller uses: + +| Component | Service (port) | Reached by | Hostname to route | +| ------------------- | --------------------------------------------- | -------------------------------- | ----------------------------- | +| Enterprise Cloud UI | `stacklok-enterprise-toolhive-cloud-ui` (80) | Browsers | `betterAuth.url` | +| Enterprise Manager | `stacklok-enterprise-enterprise-manager` (80) | Stacklok Desktop and CLI clients | `resourceURL` | +| Registry Server | `registry-api` (8080) | Stacklok Desktop and CLI clients | the registry's public API URL | + +The in-cluster URLs (`apiBaseUrl`, `enterpriseManagerUrl`) stay as Service DNS +and need no routing. Once the routes resolve, confirm the Enterprise Manager +answers at its external hostname: + +```bash +curl -sf https://config.example.com/.well-known/toolhive-configuration | jq . +``` + +### 7. Prepare workload namespaces + +If you run MCP server and vMCP workloads in a namespace other than +`stacklok-system` (the recommended setup, see the Namespaces note above), copy +the image pull secret into that namespace. The operator stamps the +`enterprise-pull-secret` secret onto every workload pod it spawns, and the +kubelet resolves it in the pod's own namespace. The Replicated integration +creates it only in `stacklok-system`: + +```bash +kubectl create namespace + +# Copy the pull secret from the platform namespace. +kubectl get secret enterprise-pull-secret -n stacklok-system \ + -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d \ + | kubectl create secret generic enterprise-pull-secret \ + --namespace \ + --type kubernetes.io/dockerconfigjson \ + --from-file=.dockerconfigjson=/dev/stdin +``` + +Repeat for any namespace that hosts operator-managed workloads. If you run +everything in `stacklok-system`, skip this step. + +## Next steps + +- [Configure policies](../enterprise-manager/policies/) to control client + behavior across your organization +- [Verify the distribution](./verify-artifacts.mdx) to confirm the signatures, + provenance, and SBOMs of the images you pulled +- [Browse the catalog](../enterprise-cloud-ui/browse-catalog.mdx) once the Cloud + UI is running + +## Related information + +- [Distributed deployments](./distributed-deployments.mdx) to spread components + across clusters or maintain separate registries per environment +- [Install from a private registry (air-gapped)](./airgap-install.mdx) for + clusters that can't reach Replicated at install time +- [Configure platform identity](./configure-identity.mdx) - the identity + provider configuration this deployment depends on as a prerequisite diff --git a/docs/platform/enterprise-platform/distributed-deployments.mdx b/docs/platform/enterprise-platform/distributed-deployments.mdx new file mode 100644 index 00000000..e862b980 --- /dev/null +++ b/docs/platform/enterprise-platform/distributed-deployments.mdx @@ -0,0 +1,232 @@ +--- +title: Distributed deployments +description: + Spread Stacklok Enterprise components across clusters or environments by + enabling only the components each one needs. +--- + +The [standard deployment](./deployment.mdx) installs every platform component in +a single Kubernetes cluster. That is the recommended setup, but the umbrella +chart doesn't require it. Each component can be turned on or off independently, +so you can install the same chart multiple times, each release enabling only the +components that belong in that cluster or environment. + +This page covers two common distributed topologies. Both use the same umbrella +chart, the same onboarding artifacts, and the same configuration keys documented +on the per-component pages; only the set of enabled components changes between +releases. + +## How it works + +Every component has its own enable flag in the umbrella chart's values: + +| Enable flag | Component | +| ------------------- | ------------------------------------------------------ | +| `toolhiveOperator` | ToolHive operator (runs MCP workloads) | +| `enterpriseManager` | Enterprise Manager (serves policies and feature flags) | +| `cloudUi` | Enterprise Cloud UI | +| `registryServer` | Registry Server (MCP server and skills catalog) | +| `aiGateway` | AI Gateway operator | + +To build a distributed topology, install the chart once per cluster or +environment, each time with a `values.yaml` that enables only the components +that release should run. Components in different clusters reach each other over +their external URLs, the same way the Stacklok clients do, so wire cross-cluster +references to ingress hostnames rather than in-cluster Service DNS. + +:::note + +Configure each enabled component exactly as the per-component pages describe. +See [Configure the Enterprise Manager](../enterprise-manager/configure.mdx), +[Configure the Enterprise Cloud UI](../enterprise-cloud-ui/configure.mdx), and +[Configure the Registry Server](./configure-registry-server.mdx). + +::: + +## Centralized control plane, operators in workload clusters + +Run the shared services once in a control cluster and run the operator in each +workload cluster that hosts MCP server workloads. Developers and clients talk to +a single Enterprise Manager, Registry Server, and Cloud UI, while MCP servers +run close to the teams and data that need them. + +```mermaid +flowchart TB + subgraph Control["Control cluster"] + EM["Enterprise Manager"] + RS["Registry Server"] + CloudUI["Enterprise Cloud UI"] + end + + subgraph WL1["Workload cluster A"] + Op1["ToolHive operator"] + end + + subgraph WL2["Workload cluster B"] + Op2["ToolHive operator"] + end + + Clients["Stacklok Desktop & CLI"] + + Clients -->|"policy + catalog"| Control + Op1 -->|"catalog source"| RS + Op2 -->|"catalog source"| RS +``` + +In the control cluster, enable the shared services and leave the operator off if +no MCP workloads run there: + +```yaml title="control-cluster-values.yaml" +toolhiveOperator: + enabled: false +enterpriseManager: + enabled: true +cloudUi: + enabled: true +registryServer: + enabled: true +aiGateway: + enabled: false + +# Configure each enabled component as its per-component page describes. +``` + +In each workload cluster, enable only the operator: + +```yaml title="workload-cluster-values.yaml" +toolhiveOperator: + enabled: true +enterpriseManager: + enabled: false +cloudUi: + enabled: false +registryServer: + enabled: false +aiGateway: + enabled: false +``` + +The operator in each workload cluster is the standard enterprise operator, so +manage its MCP server and Virtual MCP Server workloads with the +[Kubernetes operator guides](../../toolhive/guides-k8s/run-mcp-k8s.mdx). Point +those workloads at the control cluster's Registry Server through its external +URL, and roll out the clients against the control cluster's Enterprise Manager. + +## Layered registries for promotion across environments + +Promote MCP servers and skills through a sequence of environments, for example +development, staging, and production, as they pass each stage's review. This +topology leans on three concepts that the Registry Server keeps separate: + +- A **Registry Server** is the instance you deploy. The Cloud UI connects to a + single instance. +- A **registry** is a named, claim-scoped catalog that an instance serves. One + instance can host several, and the Cloud UI shows each user the registries + their claims allow as a dropdown. +- A **source** is where a registry's entries come from. The `api` source type + pulls from another Registry Server, since every instance implements the + standard registry API. + +Run a Registry Server in each environment so servers can be reviewed and +promoted in place. The production instance does double duty: alongside its own +gated `production` registry, it defines `staging` and `development` registries +whose entries come from `api` sources pointing at those environments' instances. +Point the Cloud UI at the production instance, and scope each registry with +identity claims so reviewers see the staging and development catalogs while +everyone sees production. + +:::note + +The per-registry claims drive the Cloud UI dropdown: a user sees only the +registries whose claims match their identity. The common pattern surfaces +development and staging to developers and reviewers while production stays +broadly visible. + +::: + +```mermaid +flowchart TB + CloudUI["Enterprise Cloud UI"] + + subgraph Prod["Production Registry Server"] + RegProd["production registry"] + RegStage["staging registry"] + RegDev["development registry"] + end + + Stage["Registry Server (staging)"] + Dev["Registry Server (dev)"] + + CloudUI -->|"catalog API"| Prod + RegStage -.->|"api source"| Stage + RegDev -.->|"api source"| Dev +``` + +Install a registry-only release in each upstream environment: + +```yaml title="env-registry-values.yaml" +toolhiveOperator: + enabled: false +enterpriseManager: + enabled: false +cloudUi: + enabled: false +registryServer: + enabled: true +aiGateway: + enabled: false + +# Registry Server configuration. See "Configure the Registry Server". +toolhive-registry-server: + upstream: + config: + # sources, registries, and database wiring for this environment +``` + +Install the production release with the Cloud UI, then point the Cloud UI at +this instance's own Registry Server: + +```yaml title="prod-values.yaml" +toolhiveOperator: + enabled: false +enterpriseManager: + enabled: true +cloudUi: + enabled: true +registryServer: + enabled: true +aiGateway: + enabled: false + +toolhive-cloud-ui: + # Point at this instance's own Registry Server. + apiBaseUrl: 'http://registry-api.stacklok-system.svc.cluster.local:8080' + +toolhive-registry-server: + upstream: + config: + # Define a local, gated `production` registry, plus `staging` and + # `development` registries backed by `api` sources that point at those + # environments' Registry Servers. Scope each registry with identity + # claims so the Cloud UI dropdown shows the right catalog to each user. + # See "Configure the Registry Server". +``` + +Configure the sources, registries, and sync policies on each Registry Server +with the open source +[Registry Server configuration](../../toolhive/guides-registry/configuration.mdx) +reference. The enterprise build reads the same configuration schema. + +If you'd rather not have the production instance carry the Cloud UI and the +cross-environment `api` sources, deploy a dedicated Registry Server instance for +aggregation and point the Cloud UI at it instead. The registry model is the +same; only the instance hosting the aggregating registries changes. + +## Next steps + +- [Deploy the platform](./deployment.mdx) for the single-cluster install the + distributed topologies build on +- [Configure the Registry Server](./configure-registry-server.mdx) to wire + sources, registries, and the database for each registry release +- [Configure platform identity](./configure-identity.mdx) so every cluster's + components share one identity provider diff --git a/docs/platform/enterprise-platform/index.mdx b/docs/platform/enterprise-platform/index.mdx new file mode 100644 index 00000000..ed79cc34 --- /dev/null +++ b/docs/platform/enterprise-platform/index.mdx @@ -0,0 +1,89 @@ +--- +title: Platform setup +description: + Deploy Stacklok Enterprise as a single umbrella Helm chart that bundles the + operator, Cloud UI, Enterprise Manager, and supporting CRDs. +--- + +import DocCardList from '@theme/DocCardList'; + +:::enterprise + +Stacklok Enterprise ships as one umbrella Helm chart that bundles every platform +component, so you install the whole platform in a single Helm release rather +than wiring up each chart yourself. + +[Learn more about Stacklok Enterprise](../index.mdx). + +::: + +## How the platform fits together + +The platform components run together in your Kubernetes cluster. The Enterprise +Manager serves policy to the Stacklok clients, the Registry Server holds the +approved MCP server and skills catalog, the Enterprise Cloud UI manages that +catalog, and the ToolHive Operator reconciles MCP server workloads. Your +identity provider authenticates every client and component. + +```mermaid +flowchart TB + Dev["Developer"] + Admin["Platform admin"] + IdP["Identity provider (OIDC)"] + + subgraph Enterprise["Stacklok Enterprise (Kubernetes cluster)"] + direction TB + CloudUI["Enterprise Cloud UI"] + RS["Registry Server"] + EM["Enterprise Manager"] + Operator["ToolHive Operator
(MCP server workloads)"] + end + + Client["Stacklok Desktop & CLI"] + + Admin -->|"manages catalog"| CloudUI + Dev -->|"browses catalog"| CloudUI + Admin -->|"writes policies"| EM + + CloudUI <-->|"registry API"| RS + CloudUI -.->|"feature flags"| EM + Operator -->|"catalog source"| RS + + EM <-->|"config + policies"| Client + RS -->|"approved catalog"| Client + CloudUI -->|"deep-link install"| Client + + IdP -.->|"OIDC tokens"| Enterprise + IdP -.->|"OIDC tokens"| Client +``` + +## Deployment sequence + +When you are ready to deploy, work through these steps in order. Each links to +its detailed guide. + +1. **Configure identity.** Set up your identity provider (authorization server, + audiences, scopes, claims, and OAuth clients) so the platform components and + clients can authenticate. Do this first, because deployment wires in the + client IDs and audiences you create here. See + [Configure platform identity](./configure-identity.mdx). +1. **Deploy the platform.** Install the umbrella chart, wiring in the identity + values from the previous step. The chart deploys the ToolHive operator, the + Enterprise Manager, the Enterprise Cloud UI, and the Registry Server as + subcharts. See [Deploy the platform](./deployment.mdx). +1. **Configure policies.** Use the Enterprise Manager to pin the registry, + control non-registry servers, standardize telemetry, and shape the client + experience. See [Configure policies](../enterprise-manager/policies/). +1. **Set up authorization.** Map identity-provider groups and roles to MCP + access with the enterprise authorization custom resources. See + [Enterprise authorization](../enterprise-authz/index.mdx). +1. **Roll out the clients.** Distribute + [Stacklok Desktop](../enterprise-desktop/index.mdx) or the + [Stacklok CLI](../enterprise-cli/index.mdx) to your users. +1. **Verify the catalog.** Browse the + [Enterprise Cloud UI](../enterprise-cloud-ui/index.mdx) to confirm the + end-to-end path, from catalog to client. + +## Contents + + diff --git a/docs/platform/enterprise-platform/verify-artifacts.mdx b/docs/platform/enterprise-platform/verify-artifacts.mdx new file mode 100644 index 00000000..860adee2 --- /dev/null +++ b/docs/platform/enterprise-platform/verify-artifacts.mdx @@ -0,0 +1,344 @@ +--- +title: Verify the distribution +description: + Independently verify the signatures, SLSA provenance, and SBOMs of Stacklok + Enterprise container images with cosign. +--- + +You can independently verify the Stacklok-built container images you pull +through the Replicated proxy (`image-proxy.stacklok.com`) without any access to +Stacklok's internal registry or source repository. Verification works straight +through the proxy with your license and the open source `cosign` tool. + +All Stacklok images are signed with **keyless** Cosign signing through GitHub +OIDC. There are no long-lived signing keys. Each signature is tied to the GitHub +Actions workflow identity that produced it and recorded in Sigstore's public +transparency log (Rekor). Each image also carries three signed attestations: an +SPDX SBOM, SLSA build provenance, and an OpenVEX vulnerability assessment. + +Replace these placeholders throughout: + +- `` - your Stacklok-issued license ID. +- `` - the email on your customer record. +- `` - an image name from the table in + [Which images are signed](#which-images-are-signed) (for example, `operator`). +- `` - the platform version (the chart `appVersion`) you installed. +- `` and `` - the namespace and label selector for a + running workload, when you read a digest from the cluster. + +:::note[Verification relies on the proxy's Referrers behavior] + +Customers pull images through the Replicated proxy, not directly from a public +registry. Cosign stores signatures and attestations as OCI referrers. The proxy +returns a spec-compliant `404` on the OCI Referrers endpoint, so Cosign falls +back to the referrers tag schema and verification works through the proxy with +no re-signing. If you are on an older proxy deployment and verification fails +with a `500` or `Internal Server Error` on a `/referrers/` request, contact +Stacklok. + +::: + +## Prerequisites + +- [`cosign`](https://docs.sigstore.dev/cosign/system_config/installation/) v2.x + or v3.x. This is the only tool you must install. +- An OCI client you almost certainly already have, used only to log in and look + up a digest: `docker` (or `podman`). [`oras`](https://oras.land/) or + [`crane`](https://github.com/google/go-containerregistry) work too if you + prefer. Cosign reuses this client's credential store, so there is no separate + Cosign login. +- Outbound HTTPS to `image-proxy.stacklok.com`, plus `fulcio.sigstore.dev`, + `rekor.sigstore.dev`, and `tuf-repo-cdn.sigstore.dev` for Sigstore's roots. +- A valid, non-expired license. The proxy authenticates every request against + it. Your license is available from the Stacklok install portal at + [install.stacklok.com](https://install.stacklok.com). + +## Step 1: Authenticate to the proxy + +Log in once with whatever client you already use. The username is your customer +email, and the **password is the license ID** (it is the bearer credential for +the proxy, exactly as for chart pulls). All of these write to the same +credential store (`~/.docker/config.json` or `$REGISTRY_AUTH_FILE`) that Cosign +reads, so you do not run any Cosign-specific login. + +```bash +# Docker (most common, since you already use it for image pulls). +# Paste at the password prompt. +docker login image-proxy.stacklok.com -u "" + +# Podman +podman login image-proxy.stacklok.com -u "" + +# oras +oras login image-proxy.stacklok.com -u "" --password-stdin <<<"" + +# crane +crane auth login image-proxy.stacklok.com -u "" --password-stdin <<<"" +``` + +:::tip[Keep the license ID out of your shell history] + +Pipe it on stdin instead of typing it inline: + +```bash +echo "" | docker login image-proxy.stacklok.com \ + -u "" --password-stdin +``` + +::: + +## Step 2: Resolve the image digest + +Verify **by digest**, never by a floating tag. Get the digest whichever way is +easiest, using a tool you already have. The proxy path the examples use is the +current one; the install portal prints the exact registry path for your release. + +```bash +BASE=image-proxy.stacklok.com/proxy/stacklok-enterprise/ghcr.io/stacklok/stacklok-enterprise + +# Best: read the digest of what you are ACTUALLY running, straight from the +# cluster (no extra tooling). +kubectl -n get pod -l \ + -o jsonpath='{.items[0].status.containerStatuses[0].imageID}' + +# docker, without pulling (buildx ships with modern Docker) +docker buildx imagetools inspect "$BASE/:" \ + --format '{{json .Manifest.Digest}}' | tr -d '"' + +# docker, plain (pulls the image, which you are doing anyway) +docker pull -q "$BASE/:" >/dev/null \ + && docker inspect --format '{{index .RepoDigests 0}}' "$BASE/:" + +# oras +oras resolve "$BASE/:" + +# crane +crane digest "$BASE/:" +``` + +Set `DIGEST=sha256:...` from whichever command you used. + +:::note + +Cosign can also verify a tag directly +(`cosign verify ... "$BASE/:"`) and resolves the digest itself. +Pinning to the digest you actually deployed (the `kubectl` line above) is the +stronger check, since a tag can be repointed. + +::: + +## Step 3: Verify the image + +The trust anchor is the signing identity: the signature was produced by a +workflow under +`github.com/stacklok/stacklok-enterprise-platform/.github/workflows/`, through +GitHub's OIDC issuer. The regex below matches all three image-signing workflows. + +```bash +IDENTITY='^https://github\.com/stacklok/stacklok-enterprise-platform/\.github/workflows/_release-(image|toolhive-cloud-ui|upstream-repackaged)\.yml@.*$' +ISSUER='https://token.actions.githubusercontent.com' +IMAGE="$BASE/@${DIGEST}" +``` + +### Verify the signature + +```bash +cosign verify \ + --certificate-identity-regexp="$IDENTITY" \ + --certificate-oidc-issuer="$ISSUER" \ + "$IMAGE" +``` + +A successful run prints `Verification for ... --` followed by the validated +claims, including the line "The code-signing certificate was verified using +trusted certificate authority certificates", and a JSON array of the signed +payloads. + +### Verify the SBOM attestation + +Stacklok ships an SPDX SBOM as a signed in-toto attestation on every image: + +```bash +cosign verify-attestation \ + --type spdxjson \ + --certificate-identity-regexp="$IDENTITY" \ + --certificate-oidc-issuer="$ISSUER" \ + "$IMAGE" \ + | jq -r '.payload' | base64 -d \ + | jq '{name: .predicate.name, spdxVersion: .predicate.spdxVersion, packages: (.predicate.packages | length)}' +``` + +The first lines confirm the attestation's signature, transparency-log, and +certificate checks. The optional `jq` pipeline decodes the SPDX document and +prints a summary (its name, SPDX version, and package count). Drop the `jq` +filter to see the full SBOM. + +### Verify the SLSA build provenance + +Every image carries SLSA build L3 provenance as a signed attestation (predicate +type `https://slsa.dev/provenance/v1`): + +```bash +cosign verify-attestation \ + --type slsaprovenance1 \ + --certificate-identity-regexp="$IDENTITY" \ + --certificate-oidc-issuer="$ISSUER" \ + "$IMAGE" \ + | jq -r '.payload' | base64 -d \ + | jq '.predicate.runDetails.builder.id, .predicate.buildDefinition.buildType' +``` + +`--type slsaprovenance1` selects the SLSA provenance v1.0 schema; it is not a +"level" selector. The build L3 property comes from how the provenance was +produced (isolated GitHub Actions reusable workflows, non-forgeable OIDC-bound +signing). Confirm it by checking that `runDetails.builder.id` is a trusted +`stacklok-enterprise-platform/.github/workflows/...` reusable-workflow identity +and that the certificate identity above matched. + +:::note[Provenance availability] + +Cosign-verifiable provenance is present on releases built after Stacklok +introduced it. For older releases, only a GitHub-native provenance attestation +exists, which is not customer-verifiable because it requires read access to the +private source repository. If `cosign verify-attestation --type slsaprovenance1` +reports no matching attestation, the image predates this and you can request the +provenance from Stacklok. + +::: + +### Verify the OpenVEX attestation + +Every image carries Stacklok's OpenVEX document as a signed attestation +(predicate type `https://openvex.dev/ns`). It records the assessed status of +known CVEs (for example, `not_affected` with a justification) so your scanner +can suppress false positives: + +```bash +cosign verify-attestation \ + --type openvex \ + --certificate-identity-regexp="$IDENTITY" \ + --certificate-oidc-issuer="$ISSUER" \ + "$IMAGE" \ + | jq -r '.payload' | base64 -d \ + | jq -r '.predicate.statements[] | "[\(.status)] \(.vulnerability.name) \(.justification // .impact_statement // "")"' +``` + +The first lines confirm the attestation's signature, transparency-log, and +certificate checks. The `jq` pipeline lists each CVE and its assessment. To feed +the document to a scanner that consumes OpenVEX (for example, `trivy --vex`), +write the decoded predicate to a file: + +```bash +cosign verify-attestation \ + --type openvex \ + --certificate-identity-regexp="$IDENTITY" \ + --certificate-oidc-issuer="$ISSUER" \ + "$IMAGE" \ + | jq -r '.payload' | base64 -d | jq '.predicate' > vex.json +``` + +### Inspect what is attached (optional) + +If you have [`oras`](https://oras.land/), it lists every artifact attached to +the image (signature, SBOM, attestations) in one call: + +```bash +oras discover "$IMAGE" +``` + +## Which images are signed + +| Image (``) | Signing workflow | Verifiable | +| ----------------------------------------------------------------------------------- | ---------------------------------- | :--------: | +| `operator`, `proxyrunner`, `vmcp`, `registry-api`, `toolhive-enterprise` | `_release-image.yml` | ✓ | +| `ai-gateway-operator`, `ai-gateway-main-processor`, `ai-gateway-api-key-service` | `_release-image.yml` | ✓ | +| `cloud-ui` | `_release-toolhive-cloud-ui.yml` | ✓ | +| `ai-gateway-controller`, `ai-gateway-extproc` | `_release-upstream-repackaged.yml` | ✓ | +| Third-party dependencies (Envoy Gateway, Envoy Ratelimit, Presidio, Replicated SDK) | upstream, not re-signed | n/a | + +:::note[Third-party dependency images] + +Third-party dependency images are served through the proxy for license-gated +distribution but are not re-signed by Stacklok. Verify those against their +original publishers' signatures if required. They are pulled by digest, so their +content is pinned. Their proxy paths use the upstream host, for example +`.../proxy/stacklok-enterprise/docker.io//`. + +::: + +Each third-party dependency plays a specific role in the platform: + +| Dependency | Role in the stack | +| --------------- | ---------------------------------------------------------------------------- | +| Envoy Gateway | Kubernetes Gateway API data plane that fronts the AI gateway | +| Envoy Ratelimit | Rate-limiting service backing the AI gateway's token budget enforcement | +| Presidio | Microsoft Presidio engine for PII and PCI detection and redaction in prompts | +| Replicated SDK | In-cluster SDK that reports install status and license state to Replicated | + +## Verify across all images + +To check every Stacklok-signed image for a release in one pass: + +```bash +BASE=image-proxy.stacklok.com/proxy/stacklok-enterprise/ghcr.io/stacklok/stacklok-enterprise +IDENTITY='^https://github\.com/stacklok/stacklok-enterprise-platform/\.github/workflows/_release-(image|toolhive-cloud-ui|upstream-repackaged)\.yml@.*$' +ISSUER='https://token.actions.githubusercontent.com' +VERSION='' + +for IMAGE in operator proxyrunner vmcp registry-api toolhive-enterprise cloud-ui \ + ai-gateway-operator ai-gateway-main-processor ai-gateway-api-key-service \ + ai-gateway-controller ai-gateway-extproc; do + # Digest lookup via docker buildx (swap for `crane digest` or `oras resolve`). + DIGEST=$(docker buildx imagetools inspect "$BASE/$IMAGE:$VERSION" \ + --format '{{json .Manifest.Digest}}' 2>/dev/null | tr -d '"') + [ -n "$DIGEST" ] || { echo "SKIP $IMAGE (no $VERSION)"; continue; } + if cosign verify --certificate-identity-regexp="$IDENTITY" \ + --certificate-oidc-issuer="$ISSUER" "$BASE/$IMAGE@$DIGEST" >/dev/null 2>&1; then + echo "OK $IMAGE@$DIGEST" + else + echo "FAIL $IMAGE@$DIGEST" + fi +done +``` + +## Next steps + +- [Configure platform identity](./configure-identity.mdx) to wire your identity + provider to the platform components +- [Configure policies](../enterprise-manager/policies/) to control client + behavior across your organization + +## Related information + +- [Deploy the platform](./deployment.mdx) - the standard install, which pulls + from Replicated at install time +- [Install from a private registry (air-gapped)](./airgap-install.mdx) - mirror + the chart and images into a registry you control + +## Troubleshooting + +
+`no signatures found` or `500` on a `/referrers/` request + +You are likely on an older proxy deployment. The proxy must return `404` on the +OCI Referrers endpoint so Cosign falls back to the tag schema. Contact Stacklok. + +
+ +
+`401 Unauthorized` on login or pull + +Confirm `` is the password (not the username) and that your license +is assigned to the channel serving the version. Expired licenses fail here. + +
+ +
+`certificate identity ... does not match` + +Cosign prints the actual certificate identity it found. Confirm it is a +`github.com/stacklok/stacklok-enterprise-platform/.github/workflows/...` path +signed by `https://token.actions.githubusercontent.com`. If it is not, do not +trust the image and contact Stacklok. + +
diff --git a/docs/toolhive/enterprise.mdx b/docs/platform/index.mdx similarity index 67% rename from docs/toolhive/enterprise.mdx rename to docs/platform/index.mdx index 0f578655..6a012069 100644 --- a/docs/toolhive/enterprise.mdx +++ b/docs/platform/index.mdx @@ -1,23 +1,24 @@ --- title: Stacklok Enterprise +sidebar_label: About description: - Managed ToolHive platform with governance, compliance, and security for MCP - servers at scale. + Govern LLM and MCP access, security, and visibility across your organization. --- import HubSpotForm from '@site/src/components/HubSpotForm'; import Heading from '@theme/Heading'; import BrandedList from '@site/src/components/BrandedList'; - - A hardened and production-ready distribution of ToolHive Community - +A governance platform for AI infrastructure -Securely scale MCP servers across your enterprise with Stacklok Enterprise's -signed binaries, hardened images, formal semantic versioning, backported -security patches, and turnkey identity provider integrations. Kubernetes native -and LLM agnostic. Self-hosted in your environment, governed by your policies, no -vendor lock-in. +Stacklok Enterprise gives your organization centralized control over how LLMs +and MCP-connected tools are accessed, used, and secured. It combines an AI +Gateway for model routing, budget enforcement, and broad model coverage with an +MCP Gateway for tool catalog management, connection policies, and per-user +configuration. Built on ToolHive, our popular open source MCP project, Stacklok +Enterprise brings enterprise-grade control, visibility, and security to AI +adoption. Kubernetes native and self-hosted in your environment, governed by +your policies, with no vendor lock-in. @@ -280,7 +314,7 @@ Stacklok Enterprise works with any AI coding assistant or agent that supports MCP. This includes Claude Code, GitHub Copilot, Cursor, Windsurf, VS Code, Zed, Cline, Continue, Goose, LM Studio, OpenAI Codex, and many more. Most clients support automatic configuration so developers can connect without manual setup. -[See the full client compatibility reference](./reference/client-compatibility.mdx) +[See the full client compatibility reference](../toolhive/reference/client-compatibility.mdx) for the complete list. @@ -293,8 +327,8 @@ servers maintained by Stacklok. From there, you have full control to add your own servers from public package managers, Docker images, remote URLs, or build a private registry tailored to your organization. You are never limited to Stacklok's catalog. -[See how to run MCP servers in Kubernetes](./guides-k8s/run-mcp-k8s.mdx) for the -full details. +[See how to run MCP servers in Kubernetes](../toolhive/guides-k8s/run-mcp-k8s.mdx) +for the full details. @@ -307,6 +341,6 @@ full details. ToolHive Community is free, open source, and the best way to evaluate MCP before moving to production. -[Get started with ToolHive Community →](./index.mdx) +[Get started with ToolHive Community →](../toolhive/index.mdx) ::: diff --git a/docs/platform/reference/crds/clusterplatformrole.mdx b/docs/platform/reference/crds/clusterplatformrole.mdx new file mode 100644 index 00000000..fc905470 --- /dev/null +++ b/docs/platform/reference/crds/clusterplatformrole.mdx @@ -0,0 +1,35 @@ +--- +title: ClusterPlatformRole +description: >- + Schema reference for ClusterPlatformRole, which defines what a role can do + across MCP products in Stacklok Enterprise. +toc_max_heading_level: 4 +--- + +`ClusterPlatformRole` defines what a role can do across the registered platform +products. The role is product-agnostic; per-product action vocabularies live +under `spec.productActions[]`, keyed by API group. Bind a role to principals +with [ClusterPlatformRoleBinding](./clusterplatformrolebinding.mdx) or +[PlatformRoleBinding](./platformrolebinding.mdx), and attach it to an MCP target +with [ToolhiveAuthorizationPolicy](./toolhiveauthorizationpolicy.mdx). + +**API:** `platform.enterprise.stacklok.com/v1alpha1` · **Scope:** Cluster · +**Short names:** `cpr`, `clusterplatformrole` + +## Example + +```yaml title="clusterplatformrole.yaml" +apiVersion: platform.enterprise.stacklok.com/v1alpha1 +kind: ClusterPlatformRole +metadata: + name: my-clusterplatformrole +spec: + productActions: + - actions: + - + apiGroup: +``` + +## Schema + + diff --git a/docs/platform/reference/crds/clusterplatformrolebinding.mdx b/docs/platform/reference/crds/clusterplatformrolebinding.mdx new file mode 100644 index 00000000..f385ef67 --- /dev/null +++ b/docs/platform/reference/crds/clusterplatformrolebinding.mdx @@ -0,0 +1,36 @@ +--- +title: ClusterPlatformRoleBinding +description: >- + Schema reference for ClusterPlatformRoleBinding, which maps IdP groups and + roles to a ClusterPlatformRole cluster-wide. +toc_max_heading_level: 4 +--- + +`ClusterPlatformRoleBinding` maps IdP groups and roles (read from the configured +`groups_claim` and `roles_claim` on the incoming JWT) to a +[ClusterPlatformRole](./clusterplatformrole.mdx), effective across every +namespace. Use [PlatformRoleBinding](./platformrolebinding.mdx) when you want a +namespace-scoped grant instead. + +**API:** `platform.enterprise.stacklok.com/v1alpha1` · **Scope:** Cluster · +**Short names:** `cprb`, `clusterplatformrolebinding` + +## Example + +```yaml title="clusterplatformrolebinding.yaml" +apiVersion: platform.enterprise.stacklok.com/v1alpha1 +kind: ClusterPlatformRoleBinding +metadata: + name: my-clusterplatformrolebinding +spec: + bindings: + - from: + - {} + roleRef: + kind: ClusterPlatformRole + name: +``` + +## Schema + + diff --git a/docs/platform/reference/crds/index.mdx b/docs/platform/reference/crds/index.mdx new file mode 100644 index 00000000..d39c4c3f --- /dev/null +++ b/docs/platform/reference/crds/index.mdx @@ -0,0 +1,54 @@ +--- +title: Platform CRD reference +description: + Reference for Stacklok Enterprise authorization custom resource definitions. +--- + +import DocCard from '@theme/DocCard'; + +The Stacklok Enterprise platform extends the Kubernetes API with custom +resources for role-based access control and authorization policy enforcement. +Each page below documents one resource - its fields, defaults, validation rules, +and a minimal example manifest - and links to the other resources it references. + +## Enterprise authorization + +
+ + + + + + + + + +
diff --git a/docs/platform/reference/crds/platformrolebinding.mdx b/docs/platform/reference/crds/platformrolebinding.mdx new file mode 100644 index 00000000..6a405272 --- /dev/null +++ b/docs/platform/reference/crds/platformrolebinding.mdx @@ -0,0 +1,38 @@ +--- +title: PlatformRoleBinding +description: >- + Schema reference for PlatformRoleBinding, which maps IdP groups and roles to a + ClusterPlatformRole within a single namespace. +toc_max_heading_level: 4 +--- + +`PlatformRoleBinding` is the namespace-scoped sibling of +[ClusterPlatformRoleBinding](./clusterplatformrolebinding.mdx). Namespace owners +use it to grant a [ClusterPlatformRole](./clusterplatformrole.mdx) to IdP +principals for the +[ToolhiveAuthorizationPolicy](./toolhiveauthorizationpolicy.mdx) resources in +the same namespace, without involving the cluster admin. + +**API:** `platform.enterprise.stacklok.com/v1alpha1` · **Scope:** Namespaced · +**Short names:** `prb`, `platformrolebinding` + +## Example + +```yaml title="platformrolebinding.yaml" +apiVersion: platform.enterprise.stacklok.com/v1alpha1 +kind: PlatformRoleBinding +metadata: + name: my-platformrolebinding + namespace: default +spec: + bindings: + - from: + - {} + roleRef: + kind: ClusterPlatformRole + name: +``` + +## Schema + + diff --git a/docs/platform/reference/crds/toolhiveauthorizationpolicy.mdx b/docs/platform/reference/crds/toolhiveauthorizationpolicy.mdx new file mode 100644 index 00000000..a11db9ec --- /dev/null +++ b/docs/platform/reference/crds/toolhiveauthorizationpolicy.mdx @@ -0,0 +1,33 @@ +--- +title: ToolhiveAuthorizationPolicy +description: >- + Schema reference for ToolhiveAuthorizationPolicy, which attaches roles and + deny rules to a specific MCP target. +toc_max_heading_level: 4 +--- + +`ToolhiveAuthorizationPolicy` attaches one or more +[ClusterPlatformRole](./clusterplatformrole.mdx) bindings to a specific MCP +target: an `MCPServer` or `MCPRemoteProxy`. Bindings can be narrowed by rule +restrictions or tool-hint filters, and hard `deny` rules compile to Cedar +`forbid` and override every grant. + +**API:** `toolhive.enterprise.stacklok.com/v1alpha1` · **Scope:** Namespaced · +**Short names:** `tap`, `authzpolicy` + +## Example + +```yaml title="toolhiveauthorizationpolicy.yaml" +apiVersion: toolhive.enterprise.stacklok.com/v1alpha1 +kind: ToolhiveAuthorizationPolicy +metadata: + name: my-toolhiveauthorizationpolicy + namespace: default +spec: + targetRef: + name: +``` + +## Schema + + diff --git a/docs/theme-preview.mdx b/docs/theme-preview.mdx index ad5a2cc3..da556e9b 100644 --- a/docs/theme-preview.mdx +++ b/docs/theme-preview.mdx @@ -2,7 +2,6 @@ title: Theme preview page description: This is a page used to preview a lot of theme elements in one place. -displayed_sidebar: toolhiveSidebar pagination_next: toolhive/guides-cli/install pagination_prev: toolhive/index --- @@ -190,7 +189,7 @@ Stacklok Enterprise includes turnkey integrations for common identity providers. Instead of manually configuring OIDC, use the built-in Okta or Entra ID integration to map IdP groups directly to ToolHive roles and policy sets. -[Learn more about Stacklok Enterprise](/toolhive/enterprise). +[Learn more about Stacklok Enterprise](./platform/index.mdx). ::: diff --git a/docs/toolhive/concepts/auth-framework.mdx b/docs/toolhive/concepts/auth-framework.mdx index 740cdb07..e5f17f05 100644 --- a/docs/toolhive/concepts/auth-framework.mdx +++ b/docs/toolhive/concepts/auth-framework.mdx @@ -250,6 +250,15 @@ all authorization checks before requests reach the server logic. This means MCP servers do not need to implement their own OAuth or custom authorization logic. ToolHive centralizes and standardizes access control. +:::enterprise + +Hand-writing Cedar policies works well for a server or two. Across a fleet, +Stacklok Enterprise lets you express access in RBAC terms (roles, bindings, and +a per-server attachment policy) and have the operator compile them to Cedar. See +[enterprise authorization](../../platform/enterprise-authz/index.mdx). + +::: + ### Why Cedar for authorization? Cedar provides several advantages for MCP server authorization: diff --git a/docs/toolhive/concepts/cedar-policies.mdx b/docs/toolhive/concepts/cedar-policies.mdx index d569c5f4..7969269e 100644 --- a/docs/toolhive/concepts/cedar-policies.mdx +++ b/docs/toolhive/concepts/cedar-policies.mdx @@ -17,6 +17,15 @@ complete dictionary of entity types, actions, and attributes, see ::: +:::enterprise + +Stacklok Enterprise compiles RBAC-style custom resources into the Cedar policies +described here, so you can manage authorization across an MCP fleet without +hand-writing a policy per server. See +[enterprise authorization](../../platform/enterprise-authz/index.mdx). + +::: + ## Cedar policy language Cedar policies express authorization rules in a clear, declarative syntax: diff --git a/docs/toolhive/faq.mdx b/docs/toolhive/faq.mdx index 23338dd7..0d1ee443 100644 --- a/docs/toolhive/faq.mdx +++ b/docs/toolhive/faq.mdx @@ -46,16 +46,16 @@ locally or in Kubernetes, free to use, with community support through GitHub and Discord. It's the right starting point for individual developers and teams evaluating MCP. -[Stacklok Enterprise](./enterprise.mdx) is a hardened, semantically versioned -distribution of ToolHive built for production use. It adds turnkey identity -provider integrations (Okta, Entra ID), built-in functionality for day-2 and -day-3 operations, backported security patches, hardened container images, an -enterprise cloud UI, and SLA-backed support. It's self-hosted, so your data -stays in your environment. +[Stacklok Enterprise](../platform/index.mdx) is a hardened, semantically +versioned distribution of ToolHive built for production use. It adds turnkey +identity provider integrations (Okta, Entra ID), built-in functionality for +day-2 and day-3 operations, backported security patches, hardened container +images, an enterprise cloud UI, and SLA-backed support. It's self-hosted, so +your data stays in your environment. If you're running MCP in production at organizational scale, need SSO and governance, or have compliance requirements, -[Stacklok Enterprise](./enterprise.mdx) is the supported path. +[Stacklok Enterprise](../platform/index.mdx) is the supported path. ### Can I use the ToolHive UI and CLI together? @@ -314,9 +314,10 @@ policies for fine-grained authorization. For details, see guides for the [CLI](./guides-cli/auth.mdx) and [Kubernetes Operator](./guides-k8s/auth-k8s.mdx). -[Stacklok Enterprise](./enterprise.mdx) adds turnkey integrations for Okta and -Entra ID, including IdP group to ToolHive role mapping and canonical policy -packs. +[Stacklok Enterprise](../platform/index.mdx) adds turnkey integrations for Okta +and Entra ID, including +[IdP group to ToolHive role mapping](../platform/enterprise-authz/index.mdx) and +canonical policy packs. ### Can I see who called what tools? @@ -340,7 +341,19 @@ catalog. If you need turnkey IdP integration, hardened images, semantic versioning, backported security patches, or SLA-backed support, see -[Stacklok Enterprise](./enterprise.mdx). +[Stacklok Enterprise](../platform/index.mdx). + +### How is Stacklok Enterprise deployed and governed? + +Stacklok Enterprise is self-hosted: you install the whole platform as a single +umbrella Helm chart in your own Kubernetes cluster, so your data never leaves +your environment. From there, the +[Enterprise Manager](../platform/enterprise-manager/index.mdx) pushes signed +policy to every client, and +[enterprise authorization](../platform/enterprise-authz/index.mdx) maps your +identity provider's groups and roles to MCP access. See +[Platform setup](../platform/enterprise-platform/index.mdx) for the end-to-end +deployment sequence. ### How do I deploy ToolHive in Kubernetes? @@ -370,6 +383,9 @@ Yes. ToolHive supports corporate environments with: [Stacklok Discord](https://discord.gg/stacklok) for community support - **Troubleshooting sections** - each guide includes troubleshooting tips for common issues +- **Stacklok Enterprise support** - if you have a + [Stacklok Enterprise](../platform/index.mdx) subscription, use your dedicated + SLA-backed support channel instead of the community resources above ### How do I report a bug or request a feature? diff --git a/docs/toolhive/guides-cli/quickstart.mdx b/docs/toolhive/guides-cli/quickstart.mdx index 877f6522..6a0742ce 100644 --- a/docs/toolhive/guides-cli/quickstart.mdx +++ b/docs/toolhive/guides-cli/quickstart.mdx @@ -313,7 +313,7 @@ ToolHive Community is great for individual use. When your organization needs centralized governance, IdP integration (Okta, Entra ID), and hardened production-ready MCP servers, that's where Stacklok Enterprise comes in. -[See what's included in Stacklok Enterprise →](../enterprise.mdx) +[See what's included in Stacklok Enterprise →](../../platform/index.mdx) ::: diff --git a/docs/toolhive/guides-cloud-ui/intro.mdx b/docs/toolhive/guides-cloud-ui/intro.mdx index 772fa368..40303d90 100644 --- a/docs/toolhive/guides-cloud-ui/intro.mdx +++ b/docs/toolhive/guides-cloud-ui/intro.mdx @@ -49,7 +49,7 @@ Stacklok Enterprise includes an enhanced Cloud UI that builds on the open source catalog with full management capabilities. Browse and install skills, and manage registry sources and entries directly from the web interface. -[Learn more about Stacklok Enterprise](../enterprise.mdx). +[Learn more about Stacklok Enterprise](../../platform/index.mdx). ::: diff --git a/docs/toolhive/guides-k8s/quickstart.mdx b/docs/toolhive/guides-k8s/quickstart.mdx index c2fb23ff..58b806e8 100644 --- a/docs/toolhive/guides-k8s/quickstart.mdx +++ b/docs/toolhive/guides-k8s/quickstart.mdx @@ -477,7 +477,7 @@ Enterprise. Enterprise adds IdP integration (Okta, Entra ID), hardened supply-chain-attested images, a Cloud UI for fleet management, and SLA-backed support - built for teams taking MCP from proof of concept to production. -[See what's included in Stacklok Enterprise →](../enterprise.mdx) +[See what's included in Stacklok Enterprise →](../../platform/index.mdx) ::: diff --git a/docs/toolhive/guides-registry/intro.mdx b/docs/toolhive/guides-registry/intro.mdx index d9ed32f7..9e928f07 100644 --- a/docs/toolhive/guides-registry/intro.mdx +++ b/docs/toolhive/guides-registry/intro.mdx @@ -12,6 +12,8 @@ multi-source aggregation. It pulls entries from multiple sources, stores them centrally, and exposes them through a standard MCP Registry API that ToolHive and other clients can consume. +The [Enterprise Cloud UI](../../platform/enterprise-cloud-ui/index.mdx) provides +a browser-based management console for the Registry Server. :::note[Registry Server vs. built-in registry] diff --git a/docs/toolhive/guides-registry/quickstart.mdx b/docs/toolhive/guides-registry/quickstart.mdx index bff9ab6e..8666e7f7 100644 --- a/docs/toolhive/guides-registry/quickstart.mdx +++ b/docs/toolhive/guides-registry/quickstart.mdx @@ -372,7 +372,7 @@ Enterprise. Enterprise adds turnkey IdP integration (Okta, Entra ID), supply-chain-attested images, and SLA-backed support for teams running an internal MCP catalog at scale. -[Learn more about Stacklok Enterprise](../enterprise.mdx). +[Learn more about Stacklok Enterprise](../../platform/index.mdx). ::: diff --git a/docs/toolhive/guides-ui/quickstart.mdx b/docs/toolhive/guides-ui/quickstart.mdx index adb2ada8..6379c5a7 100644 --- a/docs/toolhive/guides-ui/quickstart.mdx +++ b/docs/toolhive/guides-ui/quickstart.mdx @@ -149,7 +149,7 @@ server. Here are some next steps to explore: Stacklok Enterprise extends ToolHive with centralized management, IdP integration, and hardened images for teams deploying MCP at scale. -[Learn about Stacklok Enterprise →](../enterprise.mdx) +[Learn about Stacklok Enterprise →](../../platform/index.mdx) ::: diff --git a/docs/toolhive/index.mdx b/docs/toolhive/index.mdx index ef4be13b..76a7f6fe 100644 --- a/docs/toolhive/index.mdx +++ b/docs/toolhive/index.mdx @@ -2,8 +2,8 @@ title: Introduction hide_title: true description: - ToolHive helps you run, govern, and connect MCP servers across the desktop - app, CLI, Kubernetes Operator, vMCP, and Registry Server. + ToolHive helps you run, govern, and connect MCP servers and agent skills + across the desktop app, CLI, Kubernetes Operator, vMCP, and Registry Server. --- import useBaseUrl from '@docusaurus/useBaseUrl'; @@ -25,7 +25,9 @@ import ThemedImage from '@theme/ThemedImage'; # What is ToolHive? ToolHive is an enterprise-grade open source (Apache 2.0) platform for running -and managing Model Context Protocol (MCP) servers. +and managing Model Context Protocol (MCP) servers and agent skills. It is the +MCP and skills management capability of Stacklok Enterprise, available as the +open source core you can also run on its own. New to the Model Context Protocol? Start with the [MCP primer](./concepts/mcp-primer.mdx). @@ -105,7 +107,7 @@ Stacklok Enterprise builds on ToolHive with centralized management, turnkey identity provider integrations (Okta, Entra ID), hardened images, and platform capabilities for teams running MCP in production. -[Learn more about Stacklok Enterprise](./enterprise.mdx) +[Learn more about Stacklok Enterprise](../platform/index.mdx) ::: diff --git a/docs/toolhive/reference/crds/embeddingserver.mdx b/docs/toolhive/reference/crds/embeddingserver.mdx index ef1139db..795935e6 100644 --- a/docs/toolhive/reference/crds/embeddingserver.mdx +++ b/docs/toolhive/reference/crds/embeddingserver.mdx @@ -2,7 +2,6 @@ title: EmbeddingServer description: >- Schema reference for EmbeddingServer, which defines a local embedding model deployment used by the vMCP optimizer. -displayed_sidebar: toolhiveSidebar toc_max_heading_level: 4 --- diff --git a/docs/toolhive/reference/crds/index.mdx b/docs/toolhive/reference/crds/index.mdx index 3cac0857..97cec1f8 100644 --- a/docs/toolhive/reference/crds/index.mdx +++ b/docs/toolhive/reference/crds/index.mdx @@ -2,15 +2,11 @@ title: Kubernetes CRD reference description: Reference for all ToolHive Kubernetes Operator custom resource definitions. -displayed_sidebar: toolhiveSidebar --- import DocCard from '@theme/DocCard'; -The ToolHive operator manages MCP workloads using Kubernetes custom resources. -Each page below documents one resource - its fields, defaults, validation -rules, and a minimal example manifest - and links to the other resources it -references. +The ToolHive operator manages MCP workloads using Kubernetes custom resources. Each page below documents one resource - its fields, defaults, validation rules, and a minimal example manifest - and links to the other resources it references. ## Core workloads diff --git a/docs/toolhive/reference/crds/mcpauthzconfig.mdx b/docs/toolhive/reference/crds/mcpauthzconfig.mdx index 31190045..d3d3edf7 100644 --- a/docs/toolhive/reference/crds/mcpauthzconfig.mdx +++ b/docs/toolhive/reference/crds/mcpauthzconfig.mdx @@ -2,7 +2,6 @@ title: MCPAuthzConfig description: >- Schema reference for MCPAuthzConfig, which configures backend-agnostic authorization policy for MCP servers and proxies. -displayed_sidebar: toolhiveSidebar toc_max_heading_level: 4 --- diff --git a/docs/toolhive/reference/crds/mcpexternalauthconfig.mdx b/docs/toolhive/reference/crds/mcpexternalauthconfig.mdx index 82294f9f..da5380c0 100644 --- a/docs/toolhive/reference/crds/mcpexternalauthconfig.mdx +++ b/docs/toolhive/reference/crds/mcpexternalauthconfig.mdx @@ -2,7 +2,6 @@ title: MCPExternalAuthConfig description: >- Schema reference for MCPExternalAuthConfig, which configures external authentication for MCP servers and proxies. -displayed_sidebar: toolhiveSidebar toc_max_heading_level: 4 --- diff --git a/docs/toolhive/reference/crds/mcpgroup.mdx b/docs/toolhive/reference/crds/mcpgroup.mdx index afaef5a0..f34c9ab3 100644 --- a/docs/toolhive/reference/crds/mcpgroup.mdx +++ b/docs/toolhive/reference/crds/mcpgroup.mdx @@ -2,7 +2,6 @@ title: MCPGroup description: >- Schema reference for MCPGroup, which groups backend MCP servers, remote proxies, and server entries for shared configuration. -displayed_sidebar: toolhiveSidebar toc_max_heading_level: 4 --- diff --git a/docs/toolhive/reference/crds/mcpoidcconfig.mdx b/docs/toolhive/reference/crds/mcpoidcconfig.mdx index e2206303..f9e90994 100644 --- a/docs/toolhive/reference/crds/mcpoidcconfig.mdx +++ b/docs/toolhive/reference/crds/mcpoidcconfig.mdx @@ -2,7 +2,6 @@ title: MCPOIDCConfig description: >- Schema reference for MCPOIDCConfig, which configures OIDC-based incoming authentication for MCP servers. -displayed_sidebar: toolhiveSidebar toc_max_heading_level: 4 --- diff --git a/docs/toolhive/reference/crds/mcpregistry.mdx b/docs/toolhive/reference/crds/mcpregistry.mdx index 06867de2..6c37b299 100644 --- a/docs/toolhive/reference/crds/mcpregistry.mdx +++ b/docs/toolhive/reference/crds/mcpregistry.mdx @@ -2,7 +2,6 @@ title: MCPRegistry description: >- Schema reference for MCPRegistry, which declares a ToolHive Registry Server deployment managed by the operator. -displayed_sidebar: toolhiveSidebar toc_max_heading_level: 4 --- diff --git a/docs/toolhive/reference/crds/mcpremoteproxy.mdx b/docs/toolhive/reference/crds/mcpremoteproxy.mdx index aca95455..0658d5fb 100644 --- a/docs/toolhive/reference/crds/mcpremoteproxy.mdx +++ b/docs/toolhive/reference/crds/mcpremoteproxy.mdx @@ -2,7 +2,6 @@ title: MCPRemoteProxy description: >- Schema reference for MCPRemoteProxy, which proxies a remote MCP server through the operator for auth, telemetry, and tool filtering. -displayed_sidebar: toolhiveSidebar toc_max_heading_level: 4 --- diff --git a/docs/toolhive/reference/crds/mcpserver.mdx b/docs/toolhive/reference/crds/mcpserver.mdx index 9792fc40..028e46aa 100644 --- a/docs/toolhive/reference/crds/mcpserver.mdx +++ b/docs/toolhive/reference/crds/mcpserver.mdx @@ -2,7 +2,6 @@ title: MCPServer description: >- Schema reference for MCPServer, which defines a containerized MCP server managed by the ToolHive operator. -displayed_sidebar: toolhiveSidebar toc_max_heading_level: 4 --- diff --git a/docs/toolhive/reference/crds/mcpserverentry.mdx b/docs/toolhive/reference/crds/mcpserverentry.mdx index 026ea7ff..018fefd2 100644 --- a/docs/toolhive/reference/crds/mcpserverentry.mdx +++ b/docs/toolhive/reference/crds/mcpserverentry.mdx @@ -2,7 +2,6 @@ title: MCPServerEntry description: >- Schema reference for MCPServerEntry, which declares a remote MCP server as a member of an MCPGroup. -displayed_sidebar: toolhiveSidebar toc_max_heading_level: 4 --- diff --git a/docs/toolhive/reference/crds/mcptelemetryconfig.mdx b/docs/toolhive/reference/crds/mcptelemetryconfig.mdx index b81e7bec..5671b085 100644 --- a/docs/toolhive/reference/crds/mcptelemetryconfig.mdx +++ b/docs/toolhive/reference/crds/mcptelemetryconfig.mdx @@ -2,7 +2,6 @@ title: MCPTelemetryConfig description: >- Schema reference for MCPTelemetryConfig, which configures OpenTelemetry tracing, metrics, and logging for MCP workloads. -displayed_sidebar: toolhiveSidebar toc_max_heading_level: 4 --- diff --git a/docs/toolhive/reference/crds/mcptoolconfig.mdx b/docs/toolhive/reference/crds/mcptoolconfig.mdx index fe0c696c..f3cd0246 100644 --- a/docs/toolhive/reference/crds/mcptoolconfig.mdx +++ b/docs/toolhive/reference/crds/mcptoolconfig.mdx @@ -2,7 +2,6 @@ title: MCPToolConfig description: >- Schema reference for MCPToolConfig, which filters and renames tools exposed by an MCP server. -displayed_sidebar: toolhiveSidebar toc_max_heading_level: 4 --- diff --git a/docs/toolhive/reference/crds/mcpwebhookconfig.mdx b/docs/toolhive/reference/crds/mcpwebhookconfig.mdx index 5bc9a067..d2365486 100644 --- a/docs/toolhive/reference/crds/mcpwebhookconfig.mdx +++ b/docs/toolhive/reference/crds/mcpwebhookconfig.mdx @@ -2,7 +2,6 @@ title: MCPWebhookConfig description: >- Schema reference for MCPWebhookConfig, which configures validating and mutating webhook middleware for MCP servers. -displayed_sidebar: toolhiveSidebar toc_max_heading_level: 4 --- diff --git a/docs/toolhive/reference/crds/virtualmcpcompositetooldefinition.mdx b/docs/toolhive/reference/crds/virtualmcpcompositetooldefinition.mdx index 07ca47c7..ae52a2ca 100644 --- a/docs/toolhive/reference/crds/virtualmcpcompositetooldefinition.mdx +++ b/docs/toolhive/reference/crds/virtualmcpcompositetooldefinition.mdx @@ -2,7 +2,6 @@ title: VirtualMCPCompositeToolDefinition description: >- Schema reference for VirtualMCPCompositeToolDefinition, which defines a composite tool workflow for a VirtualMCPServer. -displayed_sidebar: toolhiveSidebar toc_max_heading_level: 4 --- diff --git a/docs/toolhive/reference/crds/virtualmcpserver.mdx b/docs/toolhive/reference/crds/virtualmcpserver.mdx index b305ae6a..57180ef7 100644 --- a/docs/toolhive/reference/crds/virtualmcpserver.mdx +++ b/docs/toolhive/reference/crds/virtualmcpserver.mdx @@ -2,7 +2,6 @@ title: VirtualMCPServer description: >- Schema reference for VirtualMCPServer, which aggregates multiple MCP servers into a single virtual endpoint. -displayed_sidebar: toolhiveSidebar toc_max_heading_level: 4 --- diff --git a/docs/toolhive/reference/index.mdx b/docs/toolhive/reference/index.mdx index 29ccba22..a60cfaa1 100644 --- a/docs/toolhive/reference/index.mdx +++ b/docs/toolhive/reference/index.mdx @@ -3,7 +3,6 @@ title: Reference description: Technical reference documentation for ToolHive, including CLI commands, API specifications, CRD definitions, and registry schemas. -displayed_sidebar: toolhiveSidebar --- import DocCard from '@theme/DocCard'; diff --git a/docs/toolhive/reference/registry-schema-upstream.mdx b/docs/toolhive/reference/registry-schema-upstream.mdx index 49730db2..d3c8ee70 100644 --- a/docs/toolhive/reference/registry-schema-upstream.mdx +++ b/docs/toolhive/reference/registry-schema-upstream.mdx @@ -3,7 +3,6 @@ title: Registry JSON schema description: JSON schema reference for the ToolHive registry format, which builds on the official MCP server schema and adds skills and publisher-provided metadata. -displayed_sidebar: toolhiveSidebar --- import JSONSchemaViewer from '@theme/JSONSchemaViewer'; diff --git a/docs/toolhive/support.mdx b/docs/toolhive/support.mdx index 99036aeb..e781be4e 100644 --- a/docs/toolhive/support.mdx +++ b/docs/toolhive/support.mdx @@ -2,13 +2,22 @@ title: Getting support sidebar_label: Support description: - Get help with ToolHive through community channels, documentation search, and - support contacts. + Get help with ToolHive and Stacklok Enterprise through community channels, + documentation search, and dedicated support. --- Whether you're troubleshooting an issue, looking for guidance, or need help with -your ToolHive deployment, there are several ways to get support. This page -outlines the available resources to help you find answers quickly. +your ToolHive or Stacklok Enterprise deployment, there are several ways to get +support. This page outlines the available resources to help you find answers +quickly. + +:::enterprise + +Stacklok Enterprise customers have dedicated, SLA-backed support that's separate +from the community resources below. Jump to +[Enterprise support](#enterprise-support). + +::: ## Self-help resources @@ -113,7 +122,17 @@ This information helps the team diagnose and fix issues more quickly. ## Enterprise support -For organizations that need an enterprise distribution of ToolHive with -dedicated support, custom integrations, or assistance with large-scale -deployments, see the [Stacklok Enterprise](./enterprise.mdx) page to learn more -and get in touch. +[Stacklok Enterprise](../platform/index.mdx) includes dedicated support that +goes beyond the community channels above. Every subscription comes with: + +- Dedicated support with defined response-time SLAs for production incidents +- Proactive security advisories, plus backported patches for supported versions +- Forward-deployed engineering help with onboarding, deployment, and policy + configuration + +If you already have a subscription, use the support channel your team set up +during onboarding. For setup and day-to-day operations, see +[Platform setup](../platform/enterprise-platform/index.mdx). + +To evaluate Stacklok Enterprise or scope a proof of concept, +[get in touch through the Stacklok Enterprise page](../platform/index.mdx#schedule-a-demo). diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 62d7ddc7..4c3fa6a8 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -6,6 +6,7 @@ import type * as Preset from '@docusaurus/preset-classic'; import PrismLight from './src/utils/prismLight'; import PrismDark from './src/utils/prismDark'; import crdReferenceRemark from './plugins/crd-reference-remark/index.mjs'; +import { getCrdSets } from './scripts/lib/crd-sets.mjs'; // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) @@ -17,7 +18,7 @@ const isProductionDeploy = process.env.VERCEL_ENV === 'production'; const config: Config = { title: 'Stacklok Docs', - tagline: 'Put MCP into production', + tagline: 'Run, govern, and secure your AI agents, tools, and models', favicon: 'img/stacklok-favicon.svg', plugins: [ [ @@ -182,7 +183,12 @@ const config: Config = { [ crdReferenceRemark, { - schemaDir: path.join(__dirname, 'static', 'api-specs', 'crds'), + // Derived from the CRD sets declared in + // .github/upstream-projects.yaml - no hardcoded list to keep + // in sync when a set is added. + schemaDirs: getCrdSets().map((s) => + path.resolve(__dirname, s.out) + ), }, ], ], @@ -252,71 +258,32 @@ const config: Config = { }, items: [ { - type: 'dropdown', - label: 'Docs', + type: 'docSidebar', + sidebarId: 'platformSidebar', + label: 'Stacklok Platform', position: 'left', - items: [ - { - label: 'Home', - href: '/toolhive', - }, - { - label: 'ToolHive UI', - to: 'toolhive/guides-ui', - }, - { - label: 'ToolHive CLI', - to: 'toolhive/guides-cli', - }, - { - label: 'Kubernetes Operator', - to: 'toolhive/guides-k8s', - }, - { - label: 'Virtual MCP Server', - to: 'toolhive/guides-vmcp', - }, - { - label: 'ToolHive Registry', - to: 'toolhive/guides-registry', - }, - ], }, { - type: 'dropdown', - label: 'References', + type: 'docSidebar', + sidebarId: 'mcpSidebar', + label: 'ToolHive MCP & skills', position: 'left', - items: [ - { - label: 'ToolHive CLI commands', - to: 'toolhive/reference/cli/thv', - }, - { - label: 'ToolHive API', - to: 'toolhive/reference/api', - }, - { - label: 'ToolHive Operator CRD', - to: 'toolhive/reference/crds', - }, - { - label: 'ToolHive Registry Server API', - to: 'toolhive/reference/registry-api', - }, - { - label: 'Registry schema', - to: 'toolhive/reference/registry-schema-upstream', - }, - ], }, { - to: '/toolhive/updates', - label: 'Updates', + type: 'docSidebar', + sidebarId: 'aiGatewaySidebar', + label: 'AI Gateway', + position: 'left', + }, + { + type: 'docSidebar', + sidebarId: 'resourcesSidebar', + label: 'Resources', position: 'left', }, { - to: 'toolhive/enterprise', - label: 'Enterprise', + to: '/toolhive/updates', + label: 'Updates', position: 'left', }, { diff --git a/plugins/crd-reference-remark/index.mjs b/plugins/crd-reference-remark/index.mjs index 4171f9bc..b822f794 100644 --- a/plugins/crd-reference-remark/index.mjs +++ b/plugins/crd-reference-remark/index.mjs @@ -196,15 +196,32 @@ function buildRelatedBlocks(entry, registry) { } export default function crdReferenceRemark(options = {}) { - const schemaDir = - options.schemaDir ?? - path.resolve(process.cwd(), 'static', 'api-specs', 'crds'); + // Accept either a single schemaDir (legacy) or a list of schemaDirs. The + // registries are merged into one Kind -> entry map. CRD Kinds must be + // globally unique across dirs because we resolve schemas by Kind name; + // throw on a collision rather than letting one set silently shadow another. + const dirs = + options.schemaDirs ?? + (options.schemaDir + ? [options.schemaDir] + : [path.resolve(process.cwd(), 'static', 'api-specs', 'toolhive-crds')]); - const indexPath = path.join(schemaDir, 'index.json'); const registry = new Map(); - if (fs.existsSync(indexPath)) { + for (const schemaDir of dirs) { + const indexPath = path.join(schemaDir, 'index.json'); + if (!fs.existsSync(indexPath)) continue; const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8')); - for (const entry of entries) registry.set(entry.kind, entry); + for (const entry of entries) { + const existing = registry.get(entry.kind); + if (existing) { + throw new Error( + `crd-reference-remark: duplicate CRD Kind "${entry.kind}" in ` + + `${existing.schemaDir} and ${schemaDir}. CRD Kinds must be unique ` + + `across schema dirs because resolves by Kind name.` + ); + } + registry.set(entry.kind, { ...entry, schemaDir }); + } } return function transformer(tree, file) { @@ -230,13 +247,16 @@ export default function crdReferenceRemark(options = {}) { const entry = registry.get(kind); if (!entry) { file.message( - ` has no matching CRD in ${path.relative(process.cwd(), indexPath)}; skipping.`, + ` has no matching CRD in any registered schema dir (${dirs.map((d) => path.relative(process.cwd(), d)).join(', ')}); skipping.`, node ); return; } - const schemaPath = path.join(schemaDir, `${entry.plural}.schema.json`); + const schemaPath = path.join( + entry.schemaDir, + `${entry.plural}.schema.json` + ); let schema; try { schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8')); diff --git a/scripts/extract-crd-schemas.mjs b/scripts/extract-crd-schemas.mjs index aa46dbd3..95c6eab3 100644 --- a/scripts/extract-crd-schemas.mjs +++ b/scripts/extract-crd-schemas.mjs @@ -4,16 +4,19 @@ /* * Extract each CRD's openAPIV3Schema from the upstream ToolHive CRD YAMLs. - * For each CRD this writes three files under static/api-specs/crds/: + * For each CRD this writes three files under static/api-specs/toolhive-crds/: * .schema.json - JSON Schema (apiVersion/kind/metadata stripped) * .example.yaml - Minimal YAML skeleton covering required fields * Plus a shared index.json with metadata and a reference graph. * * Usage: - * node scripts/extract-crd-schemas.mjs [--src ] + * node scripts/extract-crd-schemas.mjs [--src ] [--out ] * * Default src is ../toolhive/deploy/charts/operator-crds/files/crds relative * to this repo. Set TOOLHIVE_CRD_DIR to override. + * Default out is static/api-specs/toolhive-crds. Set TOOLHIVE_CRD_OUT to override. + * Each src/out pair is independent; run the script twice for OSS and + * enterprise CRDs to keep their indexes separate. */ import fs from 'node:fs'; @@ -24,7 +27,6 @@ import { intros } from './lib/crd-intros.mjs'; const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); const repoRoot = path.resolve(__dirname, '..'); -const outDir = path.join(repoRoot, 'static', 'api-specs', 'crds'); const defaultSrc = path.resolve( repoRoot, @@ -36,6 +38,7 @@ const defaultSrc = path.resolve( 'files', 'crds' ); +const defaultOut = path.join(repoRoot, 'static', 'api-specs', 'toolhive-crds'); const args = process.argv.slice(2); const srcArgIdx = args.indexOf('--src'); @@ -44,6 +47,12 @@ const srcDir = ? args[srcArgIdx + 1] : process.env.TOOLHIVE_CRD_DIR || defaultSrc; +const outArgIdx = args.indexOf('--out'); +const outDir = + outArgIdx >= 0 + ? path.resolve(args[outArgIdx + 1]) + : process.env.TOOLHIVE_CRD_OUT || defaultOut; + if (!fs.existsSync(srcDir)) { console.error(`CRD source directory not found: ${srcDir}`); process.exit(1); diff --git a/scripts/generate-crd-barrel.mjs b/scripts/generate-crd-barrel.mjs new file mode 100644 index 00000000..86e0e53b --- /dev/null +++ b/scripts/generate-crd-barrel.mjs @@ -0,0 +1,111 @@ +#!/usr/bin/env node +// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/* + * Generate the consolidated CRD schema barrel consumed by the + * React component (src/components/CRDReference/all-schemas.ts). + * + * Reads every CRD set declared in .github/upstream-projects.yaml (via + * scripts/lib/crd-sets.mjs) and the per-set index.json each set's extract + * step produced, then emits one TypeScript module that imports every CRD's + * JSON Schema and maps it by Kind. There is no hand-maintained merge step: + * add a CRD set to the YAML, regenerate, and the barrel picks it up. + * + * CRD Kinds must be globally unique across sets, because both + * and the remark plugin resolve schemas by Kind name. This + * script throws on a collision rather than letting one set silently shadow + * another. + * + * Usage: node scripts/generate-crd-barrel.mjs + * (Also run automatically at the end of extract-crds.mjs.) + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { getCrdSets, repoRoot } from './lib/crd-sets.mjs'; + +const barrelPath = path.resolve( + repoRoot, + 'src', + 'components', + 'CRDReference', + 'all-schemas.ts' +); + +// Collect { kind, plural, importBase } across all sets, failing on any Kind +// that appears in more than one set. +// +// A declared set whose index.json is absent is skipped with a warning, not +// an error: regenerating one set in isolation (or first-time setup of a new +// set) is a legitimate state, and the other sets' generated files are +// committed build inputs that are normally present. This matches the +// crd-reference-remark plugin, which also skips missing schema dirs. The +// loud failure for a genuinely-missing set happens later, at Docusaurus +// build time, when a page references a Kind that isn't in the barrel. +const byKind = new Map(); +const skipped = []; +for (const set of getCrdSets()) { + const indexPath = path.resolve(repoRoot, set.out, 'index.json'); + if (!fs.existsSync(indexPath)) { + skipped.push(set.key); + continue; + } + const importBase = '@site/' + set.out.replace(/\\/g, '/'); + const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8')); + for (const entry of entries) { + const existing = byKind.get(entry.kind); + if (existing) { + throw new Error( + `Duplicate CRD Kind "${entry.kind}" in sets "${existing.key}" and ` + + `"${set.key}". CRD Kinds must be globally unique across all sets ` + + `because and resolve schemas by Kind name.` + ); + } + byKind.set(entry.kind, { + key: set.key, + plural: entry.plural, + importPath: `${importBase}/${entry.plural}.schema.json`, + }); + } +} + +// Stable alphabetical order by Kind so the generated file diffs cleanly. +const kinds = [...byKind.keys()].sort(); + +const imports = kinds + .map((kind) => `import ${kind} from '${byKind.get(kind).importPath}';`) + .join('\n'); + +const members = kinds.map((kind) => ` ${kind},`).join('\n'); + +const content = `// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// AUTO-GENERATED by scripts/generate-crd-barrel.mjs. Do not edit. +// Consolidated map of every CRD Kind to its extracted JSON Schema, across all +// CRD sets declared in .github/upstream-projects.yaml. Consumed by the +// React component; the remark plugin reads the same +// schemas from disk at build time. Regenerate with extract-crds.mjs or +// \`node scripts/generate-crd-barrel.mjs\`. + +${imports} + +export const allSchemas = { +${members} +} as const; + +export type AnyCRDKind = keyof typeof allSchemas; +`; + +fs.writeFileSync(barrelPath, content); +const setCount = getCrdSets().length - skipped.length; +console.log( + `Wrote ${path.relative(repoRoot, barrelPath)} (${kinds.length} CRD Kind(s) across ${setCount} set(s)).` +); +if (skipped.length) { + console.warn( + `Note: skipped ${skipped.length} declared set(s) with no generated index.json: ${skipped.join(', ')}. ` + + `Run extract-crds.mjs for those projects to include their CRDs in the barrel.` + ); +} diff --git a/scripts/generate-crd-pages.mjs b/scripts/generate-crd-pages.mjs index 3b828d07..d291b782 100644 --- a/scripts/generate-crd-pages.mjs +++ b/scripts/generate-crd-pages.mjs @@ -4,11 +4,40 @@ /* * Generate MDX pages for each CRD plus the CRD reference landing page and a - * sidebar.json consumed by sidebars.ts. Every CRD present in - * static/api-specs/crds/index.json (produced by extract-crd-schemas.mjs) is - * published. Hand-written overrides in scripts/lib/crd-intros.mjs are merged - * over schema-derived defaults, so a new upstream CRD ships a usable page + * sidebar.json consumed by sidebars.ts. Every CRD present in the data + * directory's index.json (produced by extract-crd-schemas.mjs) is published. + * Hand-written overrides in scripts/lib/crd-intros.mjs are merged over + * schema-derived defaults, so a new upstream CRD ships a usable page * automatically; overrides are improvements, not prerequisites. + * + * Usage: + * node scripts/generate-crd-pages.mjs [--data ] [--pages ] + * + * --data Directory produced by extract-crd-schemas.mjs (contains + * index.json and per-CRD schema/example files). + * Default: static/api-specs/toolhive-crds + * --pages Directory to write MDX pages into. + * Default: docs/toolhive/reference/crds + * --landing-title Title for the CRD reference landing page and sidebar + * category label. + * Default: "Kubernetes CRD reference" + * --landing-description Front-matter description for the landing page (used + * for DocCard previews and SEO meta). + * Default: "Reference for all ToolHive Kubernetes Operator + * custom resource definitions." + * --landing-intro Intro paragraph text rendered below the landing page + * title, above the DocCard grid. + * Default: ToolHive-specific copy. + * + * The Docusaurus doc-ID prefix (used in sidebar items and landing-page hrefs) + * is derived automatically from --pages by stripping the leading docs/ path. + * Run the script twice with different flags to publish OSS and enterprise CRDs + * to separate sections without the indexes clobbering each other. + * + * This script writes per-set output only (MDX pages, sidebar fragment, and + * the enriched index.json). The consolidated Kind -> schema barrel consumed + * by spans all sets and is generated separately by + * scripts/generate-crd-barrel.mjs. */ import fs from 'node:fs'; @@ -23,8 +52,49 @@ import { intros, groupLabels, groupOrder } from './lib/crd-intros.mjs'; const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); const repoRoot = path.resolve(__dirname, '..'); -const crdsDataDir = path.join(repoRoot, 'static', 'api-specs', 'crds'); -const pagesDir = path.join(repoRoot, 'docs', 'toolhive', 'reference', 'crds'); + +const defaultDataDir = path.join( + repoRoot, + 'static', + 'api-specs', + 'toolhive-crds' +); +const defaultPagesDir = path.join( + repoRoot, + 'docs', + 'toolhive', + 'reference', + 'crds' +); + +const args = process.argv.slice(2); + +const dataArgIdx = args.indexOf('--data'); +const crdsDataDir = + dataArgIdx >= 0 ? path.resolve(args[dataArgIdx + 1]) : defaultDataDir; + +const pagesArgIdx = args.indexOf('--pages'); +const pagesDir = + pagesArgIdx >= 0 ? path.resolve(args[pagesArgIdx + 1]) : defaultPagesDir; + +function argValue(flag, fallback) { + const idx = args.indexOf(flag); + return idx >= 0 ? args[idx + 1] : fallback; +} + +const landingTitle = argValue('--landing-title', 'Kubernetes CRD reference'); +const landingDescription = argValue( + '--landing-description', + 'Reference for all ToolHive Kubernetes Operator custom resource definitions.' +); +const landingIntro = argValue( + '--landing-intro', + 'The ToolHive operator manages MCP workloads using Kubernetes custom resources.\nEach page below documents one resource - its fields, defaults, validation\nrules, and a minimal example manifest - and links to the other resources it\nreferences.' +); + +// Derive the Docusaurus doc-ID prefix from pagesDir (strip the leading docs/). +const docsRoot = path.join(repoRoot, 'docs'); +const docIdPrefix = path.relative(docsRoot, pagesDir); // e.g. "toolhive/reference/crds" fs.mkdirSync(pagesDir, { recursive: true }); @@ -147,7 +217,6 @@ function renderPage(entry) { title: ${entry.kind} description: >- ${meta.description} -displayed_sidebar: toolhiveSidebar toc_max_heading_level: 4 --- @@ -199,42 +268,40 @@ function kindsByGroup() { function renderLandingPage() { const grouped = kindsByGroup(); - const sections = groupOrder.map((group) => { - const label = groupLabels[group]; - const cards = grouped[group] - .map( - (item) => ` grouped[group].length > 0) + .map((group) => { + const label = groupLabels[group]; + const cards = grouped[group] + .map( + (item) => `` - ) - .join('\n\n'); - return `## ${label} + ) + .join('\n\n'); + return `## ${label}
${cards}
`; - }); + }); return `--- -title: Kubernetes CRD reference +title: ${landingTitle} description: - Reference for all ToolHive Kubernetes Operator custom resource definitions. -displayed_sidebar: toolhiveSidebar + ${landingDescription} --- import DocCard from '@theme/DocCard'; -The ToolHive operator manages MCP workloads using Kubernetes custom resources. -Each page below documents one resource - its fields, defaults, validation -rules, and a minimal example manifest - and links to the other resources it -references. +${landingIntro} ${sections.join('\n\n')} `; @@ -244,19 +311,18 @@ function renderSidebarFragment() { const grouped = kindsByGroup(); return { type: 'category', - label: 'CRD reference', - description: - 'Reference for the Kubernetes custom resources managed by the ToolHive operator', - link: { type: 'doc', id: 'toolhive/reference/crds/index' }, - items: groupOrder.map((group) => ({ - type: 'category', - label: groupLabels[group], - collapsed: false, - collapsible: false, - items: grouped[group].map( - (item) => `toolhive/reference/crds/${item.slug}` - ), - })), + label: landingTitle, + description: landingDescription, + link: { type: 'doc', id: `${docIdPrefix}/index` }, + items: groupOrder + .filter((group) => grouped[group].length > 0) + .map((group) => ({ + type: 'category', + label: groupLabels[group], + collapsed: false, + collapsible: false, + items: grouped[group].map((item) => `${docIdPrefix}/${item.slug}`), + })), }; } @@ -293,39 +359,6 @@ fs.writeFileSync( ); console.log(`Wrote ${path.relative(repoRoot, sidebarPath)}`); -// Emit the schemas index consumed by . This lets MDX authors -// reference a CRD by its Kind name without knowing the plural form or -// importing the JSON themselves. -const schemasPath = path.resolve( - repoRoot, - 'src', - 'components', - 'CRDReference', - 'schemas.ts' -); -const schemasContent = `// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc. -// SPDX-License-Identifier: Apache-2.0 - -// AUTO-GENERATED by scripts/generate-crd-pages.mjs. Do not edit. -// Maps each CRD Kind to its extracted JSON Schema so and the -// remark plugin can resolve schemas by Kind name. - -${index - .map( - (entry) => - `import ${entry.kind} from '@site/static/api-specs/crds/${entry.plural}.schema.json';` - ) - .join('\n')} - -export const schemas = { -${index.map((entry) => ` ${entry.kind},`).join('\n')} -} as const; - -export type CRDKind = keyof typeof schemas; -`; -fs.writeFileSync(schemasPath, schemasContent); -console.log(`Wrote ${path.relative(repoRoot, schemasPath)}`); - const withoutOverride = index.filter( (entry) => !metaByKind.get(entry.kind).hasOverride ); diff --git a/scripts/lib/crd-intros.mjs b/scripts/lib/crd-intros.mjs index d36f9888..28e76f00 100644 --- a/scripts/lib/crd-intros.mjs +++ b/scripts/lib/crd-intros.mjs @@ -3,7 +3,7 @@ /* * Hand-written overrides for per-CRD metadata. Every CRD in - * static/api-specs/crds/index.json is published automatically; entries here + * static/api-specs/toolhive-crds/index.json is published automatically; entries here * are improvements over schema-derived defaults, not prerequisites. * * To improve a CRD's page (or intentionally position a new one on the @@ -36,10 +36,11 @@ export const groupLabels = { core: 'Core workloads', + enterpriseAuthz: 'Enterprise authorization', shared: 'Shared configuration', }; -export const groupOrder = ['core', 'shared']; +export const groupOrder = ['core', 'enterpriseAuthz', 'shared']; export const intros = { // Core workloads - ordered from primary resource outward. @@ -89,6 +90,45 @@ export const intros = { '`MCPRegistry` deploys a [ToolHive Registry Server](../../guides-registry/intro.mdx) in the cluster. The operator watches `MCPRegistry` resources and provisions the Registry Server, its PostgreSQL backing, and the configured sources (Git, ConfigMap, URL, or Kubernetes discovery) that populate its catalog of MCP server definitions.\n\n:::warning[Deprecated]\n\nThe `MCPRegistry` CRD is deprecated and will be removed in a future release. The CRD remains fully functional, but `kubectl` now prints a deprecation warning on apply or get and the operator emits a `Warning` event recommending the [`toolhive-registry-server` Helm chart](https://github.com/stacklok/toolhive-registry-server). For new deployments, see [Deploy with Helm](../../guides-registry/deploy-helm.mdx).\n\n:::', }, + // Enterprise authorization - role definition, cluster and namespaced + // bindings, then per-target policy. + ClusterPlatformRole: { + slug: 'clusterplatformrole', + group: 'enterpriseAuthz', + summary: 'Cluster-wide role definition for MCP authorization.', + description: + 'Schema reference for ClusterPlatformRole, which defines what a role can do across MCP products in Stacklok Enterprise.', + intro: + '`ClusterPlatformRole` defines what a role can do across the registered platform products. The role is product-agnostic; per-product action vocabularies live under `spec.productActions[]`, keyed by API group. Bind a role to principals with [ClusterPlatformRoleBinding](./clusterplatformrolebinding.mdx) or [PlatformRoleBinding](./platformrolebinding.mdx), and attach it to an MCP target with [ToolhiveAuthorizationPolicy](./toolhiveauthorizationpolicy.mdx).', + }, + ClusterPlatformRoleBinding: { + slug: 'clusterplatformrolebinding', + group: 'enterpriseAuthz', + summary: 'Cluster-wide binding of a role to IdP principals.', + description: + 'Schema reference for ClusterPlatformRoleBinding, which maps IdP groups and roles to a ClusterPlatformRole cluster-wide.', + intro: + '`ClusterPlatformRoleBinding` maps IdP groups and roles (read from the configured `groups_claim` and `roles_claim` on the incoming JWT) to a [ClusterPlatformRole](./clusterplatformrole.mdx), effective across every namespace. Use [PlatformRoleBinding](./platformrolebinding.mdx) when you want a namespace-scoped grant instead.', + }, + PlatformRoleBinding: { + slug: 'platformrolebinding', + group: 'enterpriseAuthz', + summary: 'Namespace-scoped binding of a role to IdP principals.', + description: + 'Schema reference for PlatformRoleBinding, which maps IdP groups and roles to a ClusterPlatformRole within a single namespace.', + intro: + '`PlatformRoleBinding` is the namespace-scoped sibling of [ClusterPlatformRoleBinding](./clusterplatformrolebinding.mdx). Namespace owners use it to grant a [ClusterPlatformRole](./clusterplatformrole.mdx) to IdP principals for the [ToolhiveAuthorizationPolicy](./toolhiveauthorizationpolicy.mdx) resources in the same namespace, without involving the cluster admin.', + }, + ToolhiveAuthorizationPolicy: { + slug: 'toolhiveauthorizationpolicy', + group: 'enterpriseAuthz', + summary: 'Attach roles and deny rules to a specific MCP target.', + description: + 'Schema reference for ToolhiveAuthorizationPolicy, which attaches roles and deny rules to a specific MCP target.', + intro: + '`ToolhiveAuthorizationPolicy` attaches one or more [ClusterPlatformRole](./clusterplatformrole.mdx) bindings to a specific MCP target: an `MCPServer` or `MCPRemoteProxy`. Bindings can be narrowed by rule restrictions or tool-hint filters, and hard `deny` rules compile to Cedar `forbid` and override every grant.', + }, + // Shared configuration - grouping, then auth, then observability/behavior, // then optimizer, then discovery. MCPGroup: { @@ -172,6 +212,6 @@ export const intros = { description: 'Schema reference for MCPAuthzConfig, which configures backend-agnostic authorization policy for MCP servers and proxies.', intro: - '`MCPAuthzConfig` defines a reusable authorization policy that is decoupled from a particular authorizer backend. [MCPServer](./mcpserver.mdx), [MCPRemoteProxy](./mcpremoteproxy.mdx), and [VirtualMCPServer](./virtualmcpserver.mdx) reference an `MCPAuthzConfig` via `spec.authzConfigRef` (or `spec.incomingAuth.authzConfigRef` on `VirtualMCPServer`), mutually exclusive with the inline `authzConfig` field.\n\n:::note[Runtime wiring is deferred]\n\nIn v0.30.0 the schema, validation, and reference tracking ship together, but workload controllers do not yet resolve `authzConfigRef` into a runtime authz config. Until a follow-up release wires that path, the field is reference-tracked (deletion protection and `status.referenceCount` work) but does **not** apply authorization. Use the inline `spec.authzConfig` field for enforcement in the meantime.\n\n:::', + '`MCPAuthzConfig` defines a reusable authorization policy that is decoupled from a particular authorizer backend. [MCPServer](./mcpserver.mdx), [MCPRemoteProxy](./mcpremoteproxy.mdx), and [VirtualMCPServer](./virtualmcpserver.mdx) reference an `MCPAuthzConfig` via `spec.authzConfigRef` (or `spec.incomingAuth.authzConfigRef` on `VirtualMCPServer`), mutually exclusive with the inline `authzConfig` field.\n\nFrom v0.30.1, workload controllers resolve `authzConfigRef` into a runtime authorization config and apply it to the proxy alongside the existing reference-tracking (deletion protection and `status.referencingWorkloads`). For walkthroughs, see [Share policies across resources with MCPAuthzConfig](../../guides-k8s/auth-k8s.mdx#share-policies-across-resources-with-mcpauthzconfig).\n\n:::note[VirtualMCPServer is Cedar-only]\n\n`VirtualMCPServer.spec.incomingAuth.authzConfigRef` only supports MCPAuthzConfig resources with `spec.type: cedarv1`. Referencing a non-Cedar config (for example, `httpv1`) fails reconciliation with a clear condition message because the vMCP runtime authorization middleware is Cedar-only.\n\n:::', }, }; diff --git a/scripts/lib/crd-sets.mjs b/scripts/lib/crd-sets.mjs new file mode 100644 index 00000000..c66ceeca --- /dev/null +++ b/scripts/lib/crd-sets.mjs @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/* + * Single source of truth for the set of CRD sets the docs site publishes. + * + * Every CRD set is declared once, under a project's `crds:` array in + * .github/upstream-projects.yaml. This module flattens those declarations so + * the generation pipeline AND the build-time consumers all read the same + * list instead of hand-maintaining parallel copies: + * + * - scripts/upstream-release/extract-crds.mjs - drives extract + generate + * - scripts/generate-crd-barrel.mjs - emits the barrel + * - docusaurus.config.ts - feeds schemaDirs to the + * crd-reference-remark plugin + * - sidebars.ts - loads each set's sidebar + * fragment for placement + * + * Adding a new CRD set is therefore a single YAML edit plus a regenerate; + * none of the consumers above need editing (sidebar placement is the only + * manual step, since where a set nests in the nav is editorial). + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import url from 'node:url'; +import yaml from 'yaml'; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); +export const repoRoot = path.resolve(__dirname, '..', '..'); +const PROJECTS_FILE = path.join(repoRoot, '.github', 'upstream-projects.yaml'); + +// Flatten every project's `crds:` entries into a normalized descriptor list. +// `key` is the basename of the output dir (e.g. "toolhive-crds"); it's the +// stable handle sidebars.ts uses to place a specific set's fragment. +export function getCrdSets() { + const parsed = yaml.parse(fs.readFileSync(PROJECTS_FILE, 'utf8')); + const sets = []; + for (const project of parsed.projects ?? []) { + for (const entry of project.crds ?? []) { + sets.push({ + projectId: project.id, + repo: project.repo, + version: project.version, + key: path.basename(entry.out), + out: entry.out, + pages: entry.pages, + source: entry.source, + releaseAsset: entry.release_asset, + landingTitle: entry.landing_title, + landingDescription: entry.landing_description, + landingIntro: entry.landing_intro, + }); + } + } + return sets; +} + +// Read a set's generated sidebar.json fragment by its key. Throws loudly if +// the fragment is missing so a forgotten regenerate fails the build instead +// of silently dropping the CRD reference from the nav. +export function loadCrdSidebar(key) { + const set = getCrdSets().find((s) => s.key === key); + if (!set) { + throw new Error( + `No CRD set with key "${key}" in ${PROJECTS_FILE}. ` + + `Known keys: ${ + getCrdSets() + .map((s) => s.key) + .join(', ') || '(none)' + }.` + ); + } + const fragmentPath = path.resolve(repoRoot, set.out, 'sidebar.json'); + if (!fs.existsSync(fragmentPath)) { + throw new Error( + `CRD sidebar fragment not found at ${fragmentPath}. ` + + `Run: node scripts/upstream-release/extract-crds.mjs --id ${set.projectId} ...` + ); + } + return JSON.parse(fs.readFileSync(fragmentPath, 'utf8')); +} diff --git a/scripts/upstream-release/extract-crds.mjs b/scripts/upstream-release/extract-crds.mjs new file mode 100644 index 00000000..6bb5273a --- /dev/null +++ b/scripts/upstream-release/extract-crds.mjs @@ -0,0 +1,167 @@ +#!/usr/bin/env node +// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/* + * Drive CRD schema extraction and MDX reference page generation for a + * given upstream project. Reads the project's CRD sets from + * .github/upstream-projects.yaml (via scripts/lib/crd-sets.mjs) and, for + * each set, runs: + * + * node scripts/extract-crd-schemas.mjs --src --out + * node scripts/generate-crd-pages.mjs --data --pages + * + * Then regenerates the consolidated schema barrel across all + * sets via scripts/generate-crd-barrel.mjs. + * + * CRD sources: + * release_asset: - download from the GitHub release, extract + * tarball to a temp dir (requires --tag) + * source: - directory inside the upstream clone + * (requires --clone) + * + * Usage: + * node scripts/upstream-release/extract-crds.mjs \ + * --id [--clone ] [--repo ] [--tag ] + * + * No-op (exits 0) if the project declares no CRD sets. + */ + +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { getCrdSets, repoRoot } from '../lib/crd-sets.mjs'; + +function parseArgs(argv) { + const args = {}; + for (let i = 0; i < argv.length; i++) { + if (argv[i] === '--id') { + args.id = argv[++i]; + } else if (argv[i] === '--clone') { + args.clone = argv[++i]; + } else if (argv[i] === '--repo') { + args.repo = argv[++i]; + } else if (argv[i] === '--tag') { + args.tag = argv[++i]; + } + } + return args; +} + +function run(cmd, args) { + execFileSync(cmd, args, { stdio: 'inherit', cwd: repoRoot }); +} + +function downloadAndExtract(releaseAsset, repo, tag) { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'extract-crds-')); + run('gh', [ + 'release', + 'download', + tag, + '--repo', + repo, + '--pattern', + releaseAsset, + '--dir', + tmp, + ]); + const downloaded = path.join(tmp, releaseAsset); + const extracted = path.join(tmp, 'crds'); + fs.mkdirSync(extracted); + run('tar', ['-xzf', downloaded, '-C', extracted]); + return { + srcDir: extracted, + cleanup: () => fs.rmSync(tmp, { recursive: true, force: true }), + }; +} + +function main() { + const { id, clone, repo: repoArg, tag } = parseArgs(process.argv.slice(2)); + if (!id) { + console.error( + 'Usage: extract-crds.mjs --id [--clone ] [--repo ] [--tag ]' + ); + process.exit(1); + } + + const sets = getCrdSets().filter((s) => s.projectId === id); + if (sets.length === 0) { + console.log(`No CRD sets declared for ${id}; nothing to extract.`); + return; + } + + for (const set of sets) { + const { out, pages } = set; + const repo = repoArg || set.repo; + const resolvedTag = tag || set.version; + + let srcDir; + let cleanup = () => {}; + + if (set.releaseAsset) { + if (!resolvedTag) { + console.error('crds release_asset entry requires --tag'); + process.exit(1); + } + ({ srcDir, cleanup } = downloadAndExtract( + set.releaseAsset, + repo, + resolvedTag + )); + } else if (set.source) { + if (!clone) { + console.error('crds source entry requires --clone'); + process.exit(1); + } + srcDir = path.join(clone, set.source); + if (!fs.existsSync(srcDir)) { + console.error(`CRD source not found in clone: ${srcDir}`); + process.exit(1); + } + } else { + console.error( + `CRD set "${set.key}" has neither release_asset nor source.` + ); + process.exit(1); + } + + try { + run('node', [ + 'scripts/extract-crd-schemas.mjs', + '--src', + srcDir, + '--out', + out, + ]); + + const generateArgs = [ + 'scripts/generate-crd-pages.mjs', + '--data', + out, + '--pages', + pages, + ]; + if (set.landingTitle) + generateArgs.push('--landing-title', set.landingTitle); + if (set.landingDescription) + generateArgs.push('--landing-description', set.landingDescription); + if (set.landingIntro) + generateArgs.push('--landing-intro', set.landingIntro); + run('node', generateArgs); + } finally { + cleanup(); + } + + console.log( + `crds: processed ${set.releaseAsset ?? set.source} -> ${out}, ${pages}` + ); + } + + // Regenerate the consolidated schema barrel across all sets. + // Reads every set's freshly-written index.json, so it reflects this run's + // changes plus the other sets as committed. + run('node', ['scripts/generate-crd-barrel.mjs']); +} + +main(); diff --git a/sidebars.ts b/sidebars.ts index c91fd518..1c8f9a4d 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -1,413 +1,546 @@ import type { SidebarsConfig } from '@docusaurus/plugin-content-docs'; import { buildCliReferenceSidebar } from './src/utils/buildHierarchicalSidebar'; -import crdSidebarJson from './static/api-specs/crds/sidebar.json'; +import { loadCrdSidebar } from './scripts/lib/crd-sets.mjs'; type SidebarItemConfig = SidebarsConfig[string] extends (infer T)[] ? T : never; -const crdSidebar = crdSidebarJson as SidebarItemConfig; +// CRD reference sidebar fragments, loaded by set key (the basename of each +// set's output dir in .github/upstream-projects.yaml). The fragments are +// generated; where each one nests in the nav below is editorial. +const crdSidebar = loadCrdSidebar('toolhive-crds') as SidebarItemConfig; +const enterpriseCrdSidebar = loadCrdSidebar( + 'enterprise-crds' +) as SidebarItemConfig; -// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) +const mcpSidebar: SidebarsConfig[string] = [ + 'toolhive/index', + { + type: 'html', + value: 'Run ToolHive', + className: 'sidebar-title', + defaultStyle: false, + }, -/** - * Creating a sidebar enables you to: - - create an ordered group of docs - - render a sidebar for each doc of that group - - provide next/previous navigation - - The sidebars can be generated from the filesystem, or explicitly defined here. - - Create as many sidebars as you want. - */ -const sidebars: SidebarsConfig = { - toolhiveSidebar: [ - 'toolhive/index', - { - type: 'html', - value: 'Run ToolHive', - className: 'sidebar-title', - defaultStyle: false, + { + type: 'category', + label: 'ToolHive UI', + description: 'How to use the ToolHive desktop application', + link: { + type: 'doc', + id: 'toolhive/guides-ui/index', }, - - { - type: 'category', - label: 'ToolHive UI', - description: 'How to use the ToolHive desktop application', - link: { - type: 'doc', - id: 'toolhive/guides-ui/index', - }, - items: [ - 'toolhive/guides-ui/quickstart', - 'toolhive/guides-ui/install', - 'toolhive/guides-ui/registry', - { - type: 'category', - label: 'Run MCP servers', - description: 'How to install and run MCP servers in the ToolHive UI', - collapsed: false, - collapsible: false, - link: { - type: 'doc', - id: 'toolhive/guides-ui/run-mcp-servers', - }, - items: [ - 'toolhive/guides-ui/group-management', - 'toolhive/guides-ui/secrets-management', - 'toolhive/guides-ui/network-isolation', - 'toolhive/guides-ui/customize-tools', - ], + items: [ + 'toolhive/guides-ui/quickstart', + 'toolhive/guides-ui/install', + 'toolhive/guides-ui/registry', + { + type: 'category', + label: 'Run MCP servers', + description: 'How to install and run MCP servers in the ToolHive UI', + collapsed: false, + collapsible: false, + link: { + type: 'doc', + id: 'toolhive/guides-ui/run-mcp-servers', }, - 'toolhive/guides-ui/client-configuration', - { - type: 'category', - label: 'Skills', - description: - 'How to install, build, and manage agent skills from the ToolHive UI', - collapsed: false, - collapsible: false, - link: { - type: 'doc', - id: 'toolhive/guides-ui/skills', - }, - items: [ - 'toolhive/guides-ui/skills-browse-install', - 'toolhive/guides-ui/skills-build', - 'toolhive/guides-ui/skills-manage', - ], + items: [ + 'toolhive/guides-ui/group-management', + 'toolhive/guides-ui/secrets-management', + 'toolhive/guides-ui/network-isolation', + 'toolhive/guides-ui/customize-tools', + ], + }, + 'toolhive/guides-ui/client-configuration', + { + type: 'category', + label: 'Skills', + description: + 'How to install, build, and manage agent skills from the ToolHive UI', + collapsed: false, + collapsible: false, + link: { + type: 'doc', + id: 'toolhive/guides-ui/skills', }, - 'toolhive/guides-ui/playground', - 'toolhive/guides-ui/cli-access', - ], - }, - - { - type: 'category', - label: 'ToolHive CLI', - description: 'How to use the ToolHive CLI for managing MCP servers', - link: { - type: 'doc', - id: 'toolhive/guides-cli/index', + items: [ + 'toolhive/guides-ui/skills-browse-install', + 'toolhive/guides-ui/skills-build', + 'toolhive/guides-ui/skills-manage', + ], }, - items: [ - 'toolhive/guides-cli/quickstart', - 'toolhive/guides-cli/install', - 'toolhive/guides-cli/registry', - { - type: 'category', - label: 'Run MCP servers', - description: 'How to run MCP servers with the ToolHive CLI', - collapsed: false, - collapsible: false, - link: { - type: 'doc', - id: 'toolhive/guides-cli/run-mcp-servers', - }, - items: [ - 'toolhive/guides-cli/manage-mcp-servers', - 'toolhive/guides-cli/group-management', - 'toolhive/guides-cli/secrets-management', - ], + 'toolhive/guides-ui/playground', + 'toolhive/guides-ui/cli-access', + ], + }, + + { + type: 'category', + label: 'ToolHive CLI', + description: 'How to use the ToolHive CLI for managing MCP servers', + link: { + type: 'doc', + id: 'toolhive/guides-cli/index', + }, + items: [ + 'toolhive/guides-cli/quickstart', + 'toolhive/guides-cli/install', + 'toolhive/guides-cli/registry', + { + type: 'category', + label: 'Run MCP servers', + description: 'How to run MCP servers with the ToolHive CLI', + collapsed: false, + collapsible: false, + link: { + type: 'doc', + id: 'toolhive/guides-cli/run-mcp-servers', }, - 'toolhive/guides-cli/client-configuration', - 'toolhive/guides-cli/skills-management', - { - type: 'category', - label: 'Permissions and security', - description: - 'How to configure filesystem and network access for MCP servers', - collapsed: false, - collapsible: false, - link: { - type: 'doc', - id: 'toolhive/guides-cli/custom-permissions', - }, - items: [ - 'toolhive/guides-cli/filesystem-access', - 'toolhive/guides-cli/thvignore', - 'toolhive/guides-cli/network-isolation', - ], + items: [ + 'toolhive/guides-cli/manage-mcp-servers', + 'toolhive/guides-cli/group-management', + 'toolhive/guides-cli/secrets-management', + ], + }, + 'toolhive/guides-cli/client-configuration', + 'toolhive/guides-cli/skills-management', + { + type: 'category', + label: 'Permissions and security', + description: + 'How to configure filesystem and network access for MCP servers', + collapsed: false, + collapsible: false, + link: { + type: 'doc', + id: 'toolhive/guides-cli/custom-permissions', }, - { - type: 'category', - label: 'Advanced workflows', - description: 'Build, test, secure, and automate', - collapsed: true, - items: [ - 'toolhive/guides-cli/auth', - 'toolhive/guides-cli/token-exchange', - 'toolhive/guides-cli/webhooks', - 'toolhive/guides-cli/telemetry-and-metrics', - 'toolhive/guides-cli/test-mcp-servers', - 'toolhive/guides-cli/build-containers', - 'toolhive/guides-cli/advanced-cicd', - { - type: 'category', - label: 'API server', - description: 'How to set up and use the ToolHive API server', - link: { - type: 'doc', - id: 'toolhive/guides-cli/api-server', - }, - items: ['toolhive/reference/api'], + items: [ + 'toolhive/guides-cli/filesystem-access', + 'toolhive/guides-cli/thvignore', + 'toolhive/guides-cli/network-isolation', + ], + }, + { + type: 'category', + label: 'Advanced workflows', + description: 'Build, test, secure, and automate', + collapsed: true, + items: [ + 'toolhive/guides-cli/auth', + 'toolhive/guides-cli/token-exchange', + 'toolhive/guides-cli/webhooks', + 'toolhive/guides-cli/telemetry-and-metrics', + 'toolhive/guides-cli/test-mcp-servers', + 'toolhive/guides-cli/build-containers', + 'toolhive/guides-cli/advanced-cicd', + { + type: 'category', + label: 'API server', + description: 'How to set up and use the ToolHive API server', + link: { + type: 'doc', + id: 'toolhive/guides-cli/api-server', }, - ], - }, - { - type: 'category', - label: 'Command reference', - description: 'Detailed reference for ToolHive CLI commands', - collapsed: true, - items: buildCliReferenceSidebar(), - }, - ], - }, - - { - type: 'category', - label: 'Kubernetes Operator', - description: 'How to deploy and manage ToolHive on Kubernetes', - link: { - type: 'doc', - id: 'toolhive/guides-k8s/index', + items: ['toolhive/reference/api'], + }, + ], }, - items: [ - 'toolhive/guides-k8s/intro', - 'toolhive/guides-k8s/quickstart', - 'toolhive/guides-k8s/deploy-operator', - 'toolhive/guides-k8s/run-mcp-k8s', - 'toolhive/guides-k8s/remote-mcp-proxy', - 'toolhive/guides-k8s/mcp-server-entry', - 'toolhive/guides-k8s/connect-clients', - 'toolhive/guides-k8s/customize-tools', - 'toolhive/guides-k8s/auth-k8s', - 'toolhive/guides-k8s/redis-session-storage', - 'toolhive/guides-k8s/rate-limiting', - 'toolhive/guides-k8s/token-exchange-k8s', - 'toolhive/guides-k8s/telemetry-and-metrics', - 'toolhive/guides-k8s/logging', - 'toolhive/guides-k8s/migrate-to-v1beta1', - crdSidebar, - ], + { + type: 'category', + label: 'Command reference', + description: 'Detailed reference for ToolHive CLI commands', + collapsed: true, + items: buildCliReferenceSidebar(), + }, + ], + }, + + { + type: 'category', + label: 'Kubernetes Operator', + description: 'How to deploy and manage ToolHive on Kubernetes', + link: { + type: 'doc', + id: 'toolhive/guides-k8s/index', }, + items: [ + 'toolhive/guides-k8s/intro', + 'toolhive/guides-k8s/quickstart', + 'toolhive/guides-k8s/deploy-operator', + 'toolhive/guides-k8s/run-mcp-k8s', + 'toolhive/guides-k8s/remote-mcp-proxy', + 'toolhive/guides-k8s/mcp-server-entry', + 'toolhive/guides-k8s/connect-clients', + 'toolhive/guides-k8s/customize-tools', + 'toolhive/guides-k8s/auth-k8s', + 'toolhive/guides-k8s/redis-session-storage', + 'toolhive/guides-k8s/rate-limiting', + 'toolhive/guides-k8s/token-exchange-k8s', + 'toolhive/guides-k8s/telemetry-and-metrics', + 'toolhive/guides-k8s/logging', + 'toolhive/guides-k8s/migrate-to-v1beta1', + crdSidebar, + ], + }, - { - type: 'html', - value: 'Platform capabilities', - className: 'sidebar-title', - defaultStyle: false, + { + type: 'html', + value: 'Platform capabilities', + className: 'sidebar-title', + defaultStyle: false, + }, + + { + type: 'category', + label: 'Virtual MCP Server', + description: + 'How to aggregate multiple MCP servers into a unified endpoint', + link: { + type: 'doc', + id: 'toolhive/guides-vmcp/index', }, + items: [ + 'toolhive/guides-vmcp/intro', + 'toolhive/guides-vmcp/quickstart', + 'toolhive/guides-vmcp/configuration', + 'toolhive/guides-vmcp/backend-discovery', + 'toolhive/guides-vmcp/authentication', + 'toolhive/guides-vmcp/tool-aggregation', + 'toolhive/guides-vmcp/composite-tools', + 'toolhive/guides-vmcp/optimizer', + 'toolhive/guides-vmcp/failure-handling', + 'toolhive/guides-vmcp/telemetry-and-metrics', + 'toolhive/guides-vmcp/audit-logging', + 'toolhive/guides-vmcp/scaling-and-performance', + 'toolhive/guides-vmcp/local-cli', + ], + }, - { - type: 'category', - label: 'Virtual MCP Server', - description: - 'How to aggregate multiple MCP servers into a unified endpoint', - link: { - type: 'doc', - id: 'toolhive/guides-vmcp/index', + { + type: 'category', + label: 'Registry Server', + description: + 'How to deploy and use the ToolHive Registry server to discover and access MCP servers and skills', + link: { + type: 'doc', + id: 'toolhive/guides-registry/index', + }, + items: [ + 'toolhive/guides-registry/intro', + 'toolhive/guides-registry/quickstart', + { + type: 'category', + label: 'Deploy the Registry Server', + link: { + type: 'doc', + id: 'toolhive/guides-registry/deployment', + }, + collapsible: false, + items: [ + 'toolhive/guides-registry/deploy-operator', + 'toolhive/guides-registry/deploy-helm', + 'toolhive/guides-registry/deploy-manual', + ], }, - items: [ - 'toolhive/guides-vmcp/intro', - 'toolhive/guides-vmcp/quickstart', - 'toolhive/guides-vmcp/configuration', - 'toolhive/guides-vmcp/backend-discovery', - 'toolhive/guides-vmcp/authentication', - 'toolhive/guides-vmcp/tool-aggregation', - 'toolhive/guides-vmcp/composite-tools', - 'toolhive/guides-vmcp/optimizer', - 'toolhive/guides-vmcp/failure-handling', - 'toolhive/guides-vmcp/telemetry-and-metrics', - 'toolhive/guides-vmcp/audit-logging', - 'toolhive/guides-vmcp/scaling-and-performance', - 'toolhive/guides-vmcp/local-cli', - ], + 'toolhive/guides-registry/configuration', + 'toolhive/guides-registry/publish-servers', + 'toolhive/guides-registry/authentication', + 'toolhive/guides-registry/authorization', + 'toolhive/guides-registry/database', + 'toolhive/guides-registry/skills', + 'toolhive/guides-registry/telemetry-metrics', + 'toolhive/guides-registry/audit-logging', + 'toolhive/reference/registry-api', + 'toolhive/reference/registry-schema-upstream', + ], + }, + + { + type: 'category', + label: 'Cloud UI', + description: + 'How to deploy and use the ToolHive Cloud UI to browse and connect MCP servers', + link: { + type: 'doc', + id: 'toolhive/guides-cloud-ui/index', }, + items: [ + 'toolhive/guides-cloud-ui/intro', + 'toolhive/guides-cloud-ui/deployment', + 'toolhive/guides-cloud-ui/configuration', + ], + }, - { - type: 'category', - label: 'Registry Server', + { + type: 'category', + label: 'MCP server guides', + description: 'How to configure and use MCP servers for different use cases', + link: { + type: 'generated-index', + slug: 'toolhive/guides-mcp', + title: 'MCP server usage guides', description: - 'How to deploy and use the ToolHive Registry server to discover and access MCP servers and skills', - link: { - type: 'doc', - id: 'toolhive/guides-registry/index', - }, - items: [ - 'toolhive/guides-registry/intro', - 'toolhive/guides-registry/quickstart', - { - type: 'category', - label: 'Deploy the Registry Server', - link: { - type: 'doc', - id: 'toolhive/guides-registry/deployment', - }, - collapsible: false, - items: [ - 'toolhive/guides-registry/deploy-helm', - 'toolhive/guides-registry/deploy-manual', - 'toolhive/guides-registry/deploy-operator', - 'toolhive/guides-registry/migrate-to-helm', - ], - }, - 'toolhive/guides-registry/configuration', - 'toolhive/guides-registry/publish-servers', - 'toolhive/guides-registry/authentication', - 'toolhive/guides-registry/authorization', - 'toolhive/guides-registry/database', - 'toolhive/guides-registry/skills', - 'toolhive/guides-registry/telemetry-metrics', - 'toolhive/guides-registry/audit-logging', - 'toolhive/reference/registry-api', - 'toolhive/reference/registry-schema-upstream', - ], + 'These guides provide step-by-step instructions for using various MCP servers with ToolHive. They cover everything from installation to advanced configuration options.', }, + items: [{ type: 'autogenerated', dirName: 'toolhive/guides-mcp' }], + }, - { - type: 'category', - label: 'Cloud UI', - description: - 'How to deploy and use the ToolHive Cloud UI to browse and discover MCP servers', - link: { + { + type: 'html', + value: 'Reference', + className: 'sidebar-title', + defaultStyle: false, + }, + + { + type: 'category', + label: 'Technical reference', + link: { + type: 'doc', + id: 'toolhive/reference/index', + }, + collapsible: false, + collapsed: false, + items: [ + 'toolhive/reference/client-compatibility', + { type: 'doc', - id: 'toolhive/guides-cloud-ui/index', + id: 'toolhive/reference/authz-policy-reference', + label: 'Authorization policies', }, - items: [ - 'toolhive/guides-cloud-ui/intro', - 'toolhive/guides-cloud-ui/deployment', - 'toolhive/guides-cloud-ui/configuration', - ], - }, + ], + }, + + 'toolhive/contributing', +]; - { - type: 'html', - value: 'Shared guides', - className: 'sidebar-title', - defaultStyle: false, +const platformSidebar: SidebarsConfig[string] = [ + 'platform/index', + + { + type: 'html', + value: 'Set up the platform', + className: 'sidebar-title', + defaultStyle: false, + }, + + { + type: 'category', + label: 'Platform setup', + link: { + type: 'doc', + id: 'platform/enterprise-platform/index', }, + items: [ + 'platform/enterprise-platform/deployment', + 'platform/enterprise-platform/distributed-deployments', + 'platform/enterprise-platform/airgap-install', + 'platform/enterprise-platform/verify-artifacts', + 'platform/enterprise-platform/configure-registry-server', + 'platform/enterprise-platform/configure-identity', + ], + }, - { - type: 'category', - label: 'Concepts', - description: 'Core concepts and architecture of ToolHive and MCP', - link: { - type: 'generated-index', - slug: 'toolhive/concepts', - description: - 'Learn about the key concepts behind ToolHive and the Model Context Protocol (MCP).', - }, - items: [ - 'toolhive/concepts/mcp-primer', - 'toolhive/concepts/groups', - 'toolhive/concepts/tool-optimization', - 'toolhive/concepts/registry-criteria', - 'toolhive/concepts/observability', - 'toolhive/concepts/auth-framework', - 'toolhive/concepts/cedar-policies', - 'toolhive/concepts/backend-auth', - 'toolhive/concepts/embedded-auth-server', - 'toolhive/concepts/vmcp', - 'toolhive/concepts/skills', - ], + { + type: 'html', + value: 'Govern the platform', + className: 'sidebar-title', + defaultStyle: false, + }, + + { + type: 'category', + label: 'Enterprise authorization', + link: { + type: 'doc', + id: 'platform/enterprise-authz/index', }, + items: [ + 'platform/enterprise-authz/intro', + 'platform/enterprise-authz/quickstart-entra', + 'platform/enterprise-authz/namespace-self-service', + ], + }, - { - type: 'category', - label: 'Integrations', - description: 'Connect ToolHive with third-party tools and services', - link: { - type: 'generated-index', - slug: 'toolhive/integrations', - description: - 'Guides for integrating ToolHive with third-party tools and services like OpenTelemetry, HashiCorp Vault, and ngrok.', - }, - items: [ - 'toolhive/integrations/opentelemetry', - 'toolhive/integrations/vault', - 'toolhive/integrations/aws-sts', - 'toolhive/integrations/ingress-ngrok', - 'toolhive/integrations/okta', - { - type: 'category', - label: 'Identity provider integration', - link: { - type: 'doc', - id: 'toolhive/integrations/vmcp-idp-overview', - }, - items: [ - 'toolhive/integrations/vmcp-entra-id', - 'toolhive/integrations/vmcp-okta', - ], - }, - ], + { + type: 'category', + label: 'Enterprise Manager', + link: { + type: 'doc', + id: 'platform/enterprise-manager/index', }, + items: [ + 'platform/enterprise-manager/intro', + 'platform/enterprise-manager/configure', + { + type: 'category', + label: 'Policies', + link: { + type: 'doc', + id: 'platform/enterprise-manager/policies/index', + }, + items: [ + 'platform/enterprise-manager/policies/registry', + 'platform/enterprise-manager/policies/non-registry-servers', + 'platform/enterprise-manager/policies/telemetry', + 'platform/enterprise-manager/policies/ca-certificate', + 'platform/enterprise-manager/policies/build-env', + 'platform/enterprise-manager/policies/desktop-app', + ], + }, + 'platform/enterprise-manager/degraded-mode', + ], + }, - { - type: 'category', - label: 'Tutorials', - description: 'End-to-end tutorials covering multiple ToolHive components', - link: { - type: 'generated-index', - slug: 'toolhive/tutorials', - description: - 'End-to-end tutorials that span multiple ToolHive components.', + enterpriseCrdSidebar, + + { + type: 'html', + value: 'Operate platform clients', + className: 'sidebar-title', + defaultStyle: false, + }, + + { + type: 'category', + label: 'Enterprise Cloud UI', + link: { + type: 'doc', + id: 'platform/enterprise-cloud-ui/index', + }, + items: [ + 'platform/enterprise-cloud-ui/intro', + 'platform/enterprise-cloud-ui/configure', + 'platform/enterprise-cloud-ui/browse-catalog', + 'platform/enterprise-cloud-ui/ai-assistant', + { + type: 'category', + label: 'Registry management', + link: { + type: 'doc', + id: 'platform/enterprise-cloud-ui/administration/index', + }, + items: [ + 'platform/enterprise-cloud-ui/administration/entries', + 'platform/enterprise-cloud-ui/administration/sources', + 'platform/enterprise-cloud-ui/administration/registries', + ], }, - items: [ - 'toolhive/tutorials/custom-registry', - 'toolhive/tutorials/mcp-optimizer', - ], + ], + }, + + { + type: 'category', + label: 'Stacklok Desktop', + link: { + type: 'doc', + id: 'platform/enterprise-desktop/index', }, + items: [ + 'platform/enterprise-desktop/intro', + { + type: 'doc', + id: 'platform/enterprise-desktop/rollout', + label: 'Rollout', + }, + 'platform/enterprise-desktop/policy-enforcement', + 'platform/enterprise-desktop/deep-links', + ], + }, + + 'platform/enterprise-cli/index', +]; + +const aiGatewaySidebar: SidebarsConfig[string] = ['ai-gateway/index']; - { - type: 'category', - label: 'MCP server guides', +const resourcesSidebar: SidebarsConfig[string] = [ + { + type: 'category', + label: 'Concepts', + description: 'Core concepts and architecture of ToolHive and MCP', + link: { + type: 'generated-index', + slug: 'toolhive/concepts', description: - 'How to configure and use MCP servers for different use cases', - link: { - type: 'generated-index', - slug: 'toolhive/guides-mcp', - title: 'MCP server usage guides', - description: - 'These guides provide step-by-step instructions for using various MCP servers with ToolHive. They cover everything from installation to advanced configuration options.', - }, - items: [{ type: 'autogenerated', dirName: 'toolhive/guides-mcp' }], + 'Learn about the key concepts behind ToolHive and the Model Context Protocol (MCP).', }, + items: [ + 'toolhive/concepts/mcp-primer', + 'toolhive/concepts/groups', + 'toolhive/concepts/tool-optimization', + 'toolhive/concepts/registry-criteria', + 'toolhive/concepts/observability', + 'toolhive/concepts/auth-framework', + 'toolhive/concepts/cedar-policies', + 'toolhive/concepts/backend-auth', + 'toolhive/concepts/embedded-auth-server', + 'toolhive/concepts/vmcp', + 'toolhive/concepts/skills', + ], + }, - { - type: 'html', - value: 'Reference', - className: 'sidebar-title', - defaultStyle: false, + { + type: 'category', + label: 'Integrations', + description: 'Connect ToolHive with third-party tools and services', + link: { + type: 'generated-index', + slug: 'toolhive/integrations', + description: + 'Guides for integrating ToolHive with third-party tools and services like OpenTelemetry, HashiCorp Vault, and ngrok.', }, - { - type: 'category', - label: 'Technical reference', - link: { - type: 'doc', - id: 'toolhive/reference/index', - }, - collapsible: false, - collapsed: false, - items: [ - 'toolhive/reference/client-compatibility', - { + items: [ + 'toolhive/integrations/opentelemetry', + 'toolhive/integrations/vault', + 'toolhive/integrations/aws-sts', + 'toolhive/integrations/ingress-ngrok', + 'toolhive/integrations/okta', + { + type: 'category', + label: 'Identity provider integration', + link: { type: 'doc', - id: 'toolhive/reference/authz-policy-reference', - label: 'Authorization policies', + id: 'toolhive/integrations/vmcp-idp-overview', }, - ], - }, - { - type: 'html', - value: 'Help', - className: 'sidebar-title', - defaultStyle: false, + items: [ + 'toolhive/integrations/vmcp-entra-id', + 'toolhive/integrations/vmcp-okta', + ], + }, + ], + }, + + { + type: 'category', + label: 'Tutorials', + description: 'End-to-end tutorials covering multiple ToolHive components', + link: { + type: 'generated-index', + slug: 'toolhive/tutorials', + description: + 'End-to-end tutorials that span multiple ToolHive components.', }, - 'toolhive/faq', - 'toolhive/enterprise', - 'toolhive/support', - 'toolhive/contributing', - ], + items: [ + 'toolhive/tutorials/custom-registry', + 'toolhive/tutorials/mcp-optimizer', + ], + }, + + { + type: 'html', + value: 'Help', + className: 'sidebar-title', + defaultStyle: false, + }, + 'toolhive/faq', + 'toolhive/support', +]; + +const sidebars: SidebarsConfig = { + platformSidebar, + mcpSidebar, + aiGatewaySidebar, + resourcesSidebar, }; export default sidebars; diff --git a/src/components/CRDReference/all-schemas.ts b/src/components/CRDReference/all-schemas.ts new file mode 100644 index 00000000..701dcb2b --- /dev/null +++ b/src/components/CRDReference/all-schemas.ts @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// AUTO-GENERATED by scripts/generate-crd-barrel.mjs. Do not edit. +// Consolidated map of every CRD Kind to its extracted JSON Schema, across all +// CRD sets declared in .github/upstream-projects.yaml. Consumed by the +// React component; the remark plugin reads the same +// schemas from disk at build time. Regenerate with extract-crds.mjs or +// `node scripts/generate-crd-barrel.mjs`. + +import ClusterPlatformRole from '@site/static/api-specs/enterprise-crds/clusterplatformroles.schema.json'; +import ClusterPlatformRoleBinding from '@site/static/api-specs/enterprise-crds/clusterplatformrolebindings.schema.json'; +import EmbeddingServer from '@site/static/api-specs/toolhive-crds/embeddingservers.schema.json'; +import MCPAuthzConfig from '@site/static/api-specs/toolhive-crds/mcpauthzconfigs.schema.json'; +import MCPExternalAuthConfig from '@site/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json'; +import MCPGroup from '@site/static/api-specs/toolhive-crds/mcpgroups.schema.json'; +import MCPOIDCConfig from '@site/static/api-specs/toolhive-crds/mcpoidcconfigs.schema.json'; +import MCPRegistry from '@site/static/api-specs/toolhive-crds/mcpregistries.schema.json'; +import MCPRemoteProxy from '@site/static/api-specs/toolhive-crds/mcpremoteproxies.schema.json'; +import MCPServer from '@site/static/api-specs/toolhive-crds/mcpservers.schema.json'; +import MCPServerEntry from '@site/static/api-specs/toolhive-crds/mcpserverentries.schema.json'; +import MCPTelemetryConfig from '@site/static/api-specs/toolhive-crds/mcptelemetryconfigs.schema.json'; +import MCPToolConfig from '@site/static/api-specs/toolhive-crds/mcptoolconfigs.schema.json'; +import MCPWebhookConfig from '@site/static/api-specs/toolhive-crds/mcpwebhookconfigs.schema.json'; +import PlatformRoleBinding from '@site/static/api-specs/enterprise-crds/platformrolebindings.schema.json'; +import ToolhiveAuthorizationPolicy from '@site/static/api-specs/enterprise-crds/toolhiveauthorizationpolicies.schema.json'; +import VirtualMCPCompositeToolDefinition from '@site/static/api-specs/toolhive-crds/virtualmcpcompositetooldefinitions.schema.json'; +import VirtualMCPServer from '@site/static/api-specs/toolhive-crds/virtualmcpservers.schema.json'; + +export const allSchemas = { + ClusterPlatformRole, + ClusterPlatformRoleBinding, + EmbeddingServer, + MCPAuthzConfig, + MCPExternalAuthConfig, + MCPGroup, + MCPOIDCConfig, + MCPRegistry, + MCPRemoteProxy, + MCPServer, + MCPServerEntry, + MCPTelemetryConfig, + MCPToolConfig, + MCPWebhookConfig, + PlatformRoleBinding, + ToolhiveAuthorizationPolicy, + VirtualMCPCompositeToolDefinition, + VirtualMCPServer, +} as const; + +export type AnyCRDKind = keyof typeof allSchemas; diff --git a/src/components/CRDReference/index.tsx b/src/components/CRDReference/index.tsx index 85953722..c519d77c 100644 --- a/src/components/CRDReference/index.tsx +++ b/src/components/CRDReference/index.tsx @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; -import { schemas, type CRDKind } from './schemas'; +import { + allSchemas as schemas, + type AnyCRDKind as CRDKind, +} from './all-schemas'; import styles from './styles.module.css'; // Minimal subset of JSON Schema we care about for CRD rendering. diff --git a/src/components/CRDReference/schemas.ts b/src/components/CRDReference/schemas.ts deleted file mode 100644 index 231d9be6..00000000 --- a/src/components/CRDReference/schemas.ts +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc. -// SPDX-License-Identifier: Apache-2.0 - -// AUTO-GENERATED by scripts/generate-crd-pages.mjs. Do not edit. -// Maps each CRD Kind to its extracted JSON Schema so and the -// remark plugin can resolve schemas by Kind name. - -import EmbeddingServer from '@site/static/api-specs/crds/embeddingservers.schema.json'; -import MCPAuthzConfig from '@site/static/api-specs/crds/mcpauthzconfigs.schema.json'; -import MCPExternalAuthConfig from '@site/static/api-specs/crds/mcpexternalauthconfigs.schema.json'; -import MCPGroup from '@site/static/api-specs/crds/mcpgroups.schema.json'; -import MCPOIDCConfig from '@site/static/api-specs/crds/mcpoidcconfigs.schema.json'; -import MCPRegistry from '@site/static/api-specs/crds/mcpregistries.schema.json'; -import MCPRemoteProxy from '@site/static/api-specs/crds/mcpremoteproxies.schema.json'; -import MCPServerEntry from '@site/static/api-specs/crds/mcpserverentries.schema.json'; -import MCPServer from '@site/static/api-specs/crds/mcpservers.schema.json'; -import MCPTelemetryConfig from '@site/static/api-specs/crds/mcptelemetryconfigs.schema.json'; -import MCPToolConfig from '@site/static/api-specs/crds/mcptoolconfigs.schema.json'; -import MCPWebhookConfig from '@site/static/api-specs/crds/mcpwebhookconfigs.schema.json'; -import VirtualMCPCompositeToolDefinition from '@site/static/api-specs/crds/virtualmcpcompositetooldefinitions.schema.json'; -import VirtualMCPServer from '@site/static/api-specs/crds/virtualmcpservers.schema.json'; - -export const schemas = { - EmbeddingServer, - MCPAuthzConfig, - MCPExternalAuthConfig, - MCPGroup, - MCPOIDCConfig, - MCPRegistry, - MCPRemoteProxy, - MCPServerEntry, - MCPServer, - MCPTelemetryConfig, - MCPToolConfig, - MCPWebhookConfig, - VirtualMCPCompositeToolDefinition, - VirtualMCPServer, -} as const; - -export type CRDKind = keyof typeof schemas; diff --git a/src/components/ProductGrid/index.tsx b/src/components/ProductGrid/index.tsx index 57b7c0b8..924c8f2a 100644 --- a/src/components/ProductGrid/index.tsx +++ b/src/components/ProductGrid/index.tsx @@ -12,7 +12,7 @@ import styles from './styles.module.css'; /** * ProductGrid layout variants */ -type GridLayout = 'auto' | 'fixed-2' | 'fixed-3' | 'fixed-4'; +type GridLayout = 'auto' | 'fixed-1' | 'fixed-2' | 'fixed-3' | 'fixed-4'; /** * ProductGrid spacing variants @@ -46,6 +46,7 @@ interface ProductGridProps { */ const gridLayouts = { auto: 'repeat(auto-fit, minmax(var(--grid-min-width, 350px), 1fr))', + 'fixed-1': 'repeat(1, 1fr)', 'fixed-2': 'repeat(2, 1fr)', 'fixed-3': 'repeat(3, 1fr)', 'fixed-4': 'repeat(4, 1fr)', diff --git a/src/components/ProductGrid/styles.module.css b/src/components/ProductGrid/styles.module.css index 13234ed9..cda192ee 100644 --- a/src/components/ProductGrid/styles.module.css +++ b/src/components/ProductGrid/styles.module.css @@ -23,6 +23,10 @@ /* Uses the default auto-fit behavior */ } +.grid--fixed-1 { + grid-template-columns: 1fr; +} + .grid--fixed-2 { grid-template-columns: repeat(2, 1fr); } diff --git a/src/css/custom.css b/src/css/custom.css index 0ad3f83d..a6d7b6f5 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -188,6 +188,55 @@ thead th { height: 22px; } +/* + * Below large-desktop widths, the long uppercase nav labels would wrap to two + * lines and crowd the logo. The wide letter-spacing is the main cause, so drop + * it back to normal once the navbar gets tight to keep the labels on one line. + */ +@media (min-width: 997px) and (max-width: 1366px) { + .navbar__link { + letter-spacing: normal; + } +} + +/* + * Below ~1220px, normal letter-spacing alone isn't enough and the labels start + * to wrap. Engage this band a little above that onset so the tightening is + * already active before wrapping would start. Reclaim the navbar and item + * padding, shrink the search box (dropping the keyboard-shortcut hint), and keep + * the logo at full size so the labels stay on one line down to the menu button + * at 996px (with enough headroom that font-rendering differences don't wrap it). + */ +@media (min-width: 997px) and (max-width: 1220px) { + :root { + --ifm-navbar-padding-horizontal: 0.75rem; + --ifm-navbar-item-padding-horizontal: 0.25rem; + } + + .navbar__brand { + flex-shrink: 0; + } + + .theme-layout-navbar .DocSearch-Button { + min-width: 0; + } + + .theme-layout-navbar .DocSearch-Button-Keys { + display: none; + } +} + +/* + * On mobile, Docusaurus collapses search to an icon and absolutely positions it + * at the top-right, where it overlapped the social icons. Hide the social icons + * in the mobile navbar (they remain in the footer) so search has room. + */ +@media (max-width: 996px) { + .navbar__items--right .navbar-icon-link { + display: none; + } +} + /* Targets the theme switch button in the navbar */ .theme-layout-navbar-right .clean-btn { color: var(--stacklok-primary-light); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 545aac98..79687b1e 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -10,7 +10,6 @@ import Head from '@docusaurus/Head'; import ProductGrid from '@site/src/components/ProductGrid'; import ProductCard from '@site/src/components/ProductCard'; import Heading from '@theme/Heading'; - import styles from './index.module.css'; function HomepageHeader() { @@ -97,7 +96,30 @@ export default function Home(): ReactNode {
- + + + Stacklok Enterprise is one platform to run, govern, and secure the + AI your teams depend on, from the MCP servers and skills your agents + call to the models behind them. + + + + - ToolHive simplifies the deployment and management of Model Context - Protocol (MCP) servers, ensuring ease of use, consistency, and - security. It's available as a standalone tool or as a - Kubernetes operator, making it versatile for various environments. + Run, secure, and govern Model Context Protocol (MCP) servers and + agent skills across the desktop app, CLI, and Kubernetes. ToolHive + is the open source core. + Govern access, cost, and compliance for the large language model + providers your teams use, with identity-bound budgets, data + guardrails, and a full audit trail. Self-hosted in your environment. + + + Stand up Stacklok Enterprise on Kubernetes and run its platform + services, the Enterprise Manager, Cloud UI, and managed clients, in + production. + + - Stacklok Enterprise provides a fully managed, enterprise-grade - platform built on ToolHive, with advanced security, compliance, and - governance features for organizations deploying MCP servers at - scale. + Centralize identity, authorization, and policy across the platform + with enterprise authorization and the Enterprise Manager. @@ -274,9 +326,9 @@ export default function Home(): ReactNode { title='MCP Optimizer' linkText='Read the docs' icon={{ - src: '/img/mcp-servers/stacklok-website-icons-efficiency-dark-green.svg', + src: '/img/icons/stacklok-website-icons-efficiency-dark-green.svg', srcDark: - '/img/mcp-servers/stacklok-website-icons-efficiency-light-green.svg', + '/img/icons/stacklok-website-icons-efficiency-light-green.svg', alt: 'Stacklok efficiency icon', width: '80px', style: { marginLeft: '1.5rem', marginBottom: '0.5rem' }, diff --git a/static/api-specs/enterprise-crds/clusterplatformrolebindings.example.yaml b/static/api-specs/enterprise-crds/clusterplatformrolebindings.example.yaml new file mode 100644 index 00000000..89bbae62 --- /dev/null +++ b/static/api-specs/enterprise-crds/clusterplatformrolebindings.example.yaml @@ -0,0 +1,11 @@ +apiVersion: platform.enterprise.stacklok.com/v1alpha1 +kind: ClusterPlatformRoleBinding +metadata: + name: my-clusterplatformrolebinding +spec: + bindings: + - from: + - {} + roleRef: + kind: ClusterPlatformRole + name: diff --git a/static/api-specs/enterprise-crds/clusterplatformrolebindings.schema.json b/static/api-specs/enterprise-crds/clusterplatformrolebindings.schema.json new file mode 100644 index 00000000..bdaeaab9 --- /dev/null +++ b/static/api-specs/enterprise-crds/clusterplatformrolebindings.schema.json @@ -0,0 +1,173 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ClusterPlatformRoleBinding", + "description": "ClusterPlatformRoleBinding binds ClusterPlatformRoles to principals cluster-wide.", + "x-kubernetes-group": "platform.enterprise.stacklok.com", + "x-kubernetes-kind": "ClusterPlatformRoleBinding", + "x-kubernetes-version": "v1alpha1", + "x-kubernetes-plural": "clusterplatformrolebindings", + "x-kubernetes-short-names": [ + "cprb", + "clusterplatformrolebinding" + ], + "x-kubernetes-scope": "Cluster", + "properties": { + "spec": { + "description": "ClusterPlatformRoleBindingSpec defines the desired state of ClusterPlatformRoleBinding.", + "properties": { + "bindings": { + "description": "Bindings is the list of role-to-principal mappings.", + "items": { + "description": "RoleBindingEntry binds a ClusterPlatformRole to a set of principals.", + "properties": { + "from": { + "description": "From is the list of principal conditions that receive the role.", + "items": { + "description": "PrincipalCondition identifies principals by group membership or role assignment.", + "properties": { + "groups": { + "description": "Groups is the list of OIDC groups a principal must belong to.", + "items": { + "maxLength": 253, + "type": "string" + }, + "maxItems": 32, + "type": "array" + }, + "roles": { + "description": "Roles is the list of OIDC roles a principal must have.", + "items": { + "maxLength": 253, + "type": "string" + }, + "maxItems": 32, + "type": "array" + } + }, + "type": "object", + "x-kubernetes-validations": [ + { + "message": "at least one of groups or roles must be non-empty", + "rule": "(has(self.groups) && size(self.groups) > 0) || (has(self.roles) && size(self.roles) > 0)" + } + ] + }, + "maxItems": 32, + "minItems": 1, + "type": "array" + }, + "roleRef": { + "description": "RoleRef references the ClusterPlatformRole to bind.", + "properties": { + "kind": { + "enum": [ + "ClusterPlatformRole" + ], + "type": "string" + }, + "name": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "kind", + "name" + ], + "type": "object" + } + }, + "required": [ + "from", + "roleRef" + ], + "type": "object" + }, + "maxItems": 32, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "bindings" + ], + "type": "object" + }, + "status": { + "description": "ClusterPlatformRoleBindingStatus defines the observed state of ClusterPlatformRoleBinding.", + "properties": { + "conditions": { + "description": "Conditions represent the latest available observations of the binding's state.", + "items": { + "description": "Condition contains details for one aspect of the current state of this API Resource.", + "properties": { + "lastTransitionTime": { + "description": "lastTransitionTime is the last time the condition transitioned from one status to another.\nThis should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.", + "format": "date-time", + "type": "string" + }, + "message": { + "description": "message is a human readable message indicating details about the transition.\nThis may be an empty string.", + "maxLength": 32768, + "type": "string" + }, + "observedGeneration": { + "description": "observedGeneration represents the .metadata.generation that the condition was set based upon.\nFor instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date\nwith respect to the current state of the instance.", + "format": "int64", + "minimum": 0, + "type": "integer" + }, + "reason": { + "description": "reason contains a programmatic identifier indicating the reason for the condition's last transition.\nProducers of specific condition types may define expected values and meanings for this field,\nand whether the values are considered a guaranteed API.\nThe value should be a CamelCase string.\nThis field may not be empty.", + "maxLength": 1024, + "minLength": 1, + "pattern": "^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$", + "type": "string" + }, + "status": { + "description": "status of the condition, one of True, False, Unknown.", + "enum": [ + "True", + "False", + "Unknown" + ], + "type": "string" + }, + "type": { + "description": "type of condition in CamelCase or in foo.example.com/CamelCase.", + "maxLength": 316, + "pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$", + "type": "string" + } + }, + "required": [ + "lastTransitionTime", + "message", + "reason", + "status", + "type" + ], + "type": "object" + }, + "type": "array", + "x-kubernetes-list-map-keys": [ + "type" + ], + "x-kubernetes-list-type": "map" + }, + "observedGeneration": { + "description": "ObservedGeneration is the most recent generation observed by the controller.", + "format": "int64", + "type": "integer" + }, + "roleCount": { + "description": "RoleCount is the number of role binding entries in Spec.Bindings.", + "format": "int32", + "type": "integer" + } + }, + "type": "object" + } + }, + "type": "object" +} diff --git a/static/api-specs/enterprise-crds/clusterplatformroles.example.yaml b/static/api-specs/enterprise-crds/clusterplatformroles.example.yaml new file mode 100644 index 00000000..63c995e3 --- /dev/null +++ b/static/api-specs/enterprise-crds/clusterplatformroles.example.yaml @@ -0,0 +1,9 @@ +apiVersion: platform.enterprise.stacklok.com/v1alpha1 +kind: ClusterPlatformRole +metadata: + name: my-clusterplatformrole +spec: + productActions: + - actions: + - + apiGroup: diff --git a/static/api-specs/enterprise-crds/clusterplatformroles.schema.json b/static/api-specs/enterprise-crds/clusterplatformroles.schema.json new file mode 100644 index 00000000..ee691988 --- /dev/null +++ b/static/api-specs/enterprise-crds/clusterplatformroles.schema.json @@ -0,0 +1,138 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ClusterPlatformRole", + "description": "ClusterPlatformRole defines a named set of product actions that can be\nbound to principals via ToolhiveAuthorizationPolicy.", + "x-kubernetes-group": "platform.enterprise.stacklok.com", + "x-kubernetes-kind": "ClusterPlatformRole", + "x-kubernetes-version": "v1alpha1", + "x-kubernetes-plural": "clusterplatformroles", + "x-kubernetes-short-names": [ + "cpr", + "clusterplatformrole" + ], + "x-kubernetes-scope": "Cluster", + "properties": { + "spec": { + "description": "ClusterPlatformRoleSpec defines the desired state of ClusterPlatformRole.", + "properties": { + "description": { + "description": "Description is a human-readable description of the role.", + "type": "string" + }, + "productActions": { + "description": "ProductActions groups action identifiers by product apiGroup so a\nsingle role can carry distinct vocabularies per product.\nMaxItems is bounded so the apiserver's CEL cost estimator does not\nmultiply the inner actions[].XValidation budget by an unbounded\nouter iteration.", + "items": { + "description": "ProductActionGroup pairs a product apiGroup with the action identifiers\ngranted under that vocabulary. A ClusterPlatformRole may carry one entry\nper product so the same role spans products without coupling the platform\nlayer to any single product's action enum.", + "properties": { + "actions": { + "description": "Actions is the list of product action identifiers granted by this\nentry. The wildcard \"*\" expands to the product's registered\nvocabulary at compile time and must be the only entry when present.", + "items": { + "maxLength": 253, + "pattern": "^[a-z][a-z0-9_]*$|^\\*$", + "type": "string" + }, + "maxItems": 64, + "minItems": 1, + "type": "array", + "x-kubernetes-validations": [ + { + "message": "wildcard '*' must be the only action when present", + "rule": "self.size() == 1 || !('*' in self)" + } + ] + }, + "apiGroup": { + "description": "APIGroup identifies the product whose vocabulary this entry uses\n(e.g. `toolhive.enterprise.stacklok.com`). Cedar compilation picks\nthe entry whose APIGroup matches the product the policy targets.", + "minLength": 1, + "type": "string" + } + }, + "required": [ + "actions", + "apiGroup" + ], + "type": "object" + }, + "maxItems": 8, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "productActions" + ], + "type": "object" + }, + "status": { + "description": "ClusterPlatformRoleStatus defines the observed state of ClusterPlatformRole.", + "properties": { + "conditions": { + "description": "Conditions represent the latest available observations of the role's state.", + "items": { + "description": "Condition contains details for one aspect of the current state of this API Resource.", + "properties": { + "lastTransitionTime": { + "description": "lastTransitionTime is the last time the condition transitioned from one status to another.\nThis should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.", + "format": "date-time", + "type": "string" + }, + "message": { + "description": "message is a human readable message indicating details about the transition.\nThis may be an empty string.", + "maxLength": 32768, + "type": "string" + }, + "observedGeneration": { + "description": "observedGeneration represents the .metadata.generation that the condition was set based upon.\nFor instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date\nwith respect to the current state of the instance.", + "format": "int64", + "minimum": 0, + "type": "integer" + }, + "reason": { + "description": "reason contains a programmatic identifier indicating the reason for the condition's last transition.\nProducers of specific condition types may define expected values and meanings for this field,\nand whether the values are considered a guaranteed API.\nThe value should be a CamelCase string.\nThis field may not be empty.", + "maxLength": 1024, + "minLength": 1, + "pattern": "^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$", + "type": "string" + }, + "status": { + "description": "status of the condition, one of True, False, Unknown.", + "enum": [ + "True", + "False", + "Unknown" + ], + "type": "string" + }, + "type": { + "description": "type of condition in CamelCase or in foo.example.com/CamelCase.", + "maxLength": 316, + "pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$", + "type": "string" + } + }, + "required": [ + "lastTransitionTime", + "message", + "reason", + "status", + "type" + ], + "type": "object" + }, + "type": "array", + "x-kubernetes-list-map-keys": [ + "type" + ], + "x-kubernetes-list-type": "map" + }, + "observedGeneration": { + "description": "ObservedGeneration is the metadata.generation last reconciled.", + "format": "int64", + "type": "integer" + } + }, + "type": "object" + } + }, + "type": "object" +} diff --git a/static/api-specs/enterprise-crds/index.json b/static/api-specs/enterprise-crds/index.json new file mode 100644 index 00000000..cc107c79 --- /dev/null +++ b/static/api-specs/enterprise-crds/index.json @@ -0,0 +1,102 @@ +[ + { + "kind": "ClusterPlatformRoleBinding", + "plural": "clusterplatformrolebindings", + "group": "platform.enterprise.stacklok.com", + "version": "v1alpha1", + "shortNames": [ + "cprb", + "clusterplatformrolebinding" + ], + "scope": "Cluster", + "description": "ClusterPlatformRoleBinding binds ClusterPlatformRoles to principals cluster-wide.", + "references": [ + { + "targetKind": "ClusterPlatformRole", + "paths": [ + "spec.bindings[].roleRef" + ] + } + ], + "referencedBy": [], + "slug": "clusterplatformrolebinding" + }, + { + "kind": "ClusterPlatformRole", + "plural": "clusterplatformroles", + "group": "platform.enterprise.stacklok.com", + "version": "v1alpha1", + "shortNames": [ + "cpr", + "clusterplatformrole" + ], + "scope": "Cluster", + "description": "ClusterPlatformRole defines a named set of product actions that can be\nbound to principals via ToolhiveAuthorizationPolicy.", + "references": [], + "referencedBy": [ + { + "sourceKind": "ClusterPlatformRoleBinding", + "paths": [ + "spec.bindings[].roleRef" + ] + }, + { + "sourceKind": "PlatformRoleBinding", + "paths": [ + "spec.bindings[].roleRef" + ] + }, + { + "sourceKind": "ToolhiveAuthorizationPolicy", + "paths": [ + "spec.bindings[].roleRef" + ] + } + ], + "slug": "clusterplatformrole" + }, + { + "kind": "PlatformRoleBinding", + "plural": "platformrolebindings", + "group": "platform.enterprise.stacklok.com", + "version": "v1alpha1", + "shortNames": [ + "prb", + "platformrolebinding" + ], + "scope": "Namespaced", + "description": "PlatformRoleBinding binds ClusterPlatformRoles to principals within a namespace.", + "references": [ + { + "targetKind": "ClusterPlatformRole", + "paths": [ + "spec.bindings[].roleRef" + ] + } + ], + "referencedBy": [], + "slug": "platformrolebinding" + }, + { + "kind": "ToolhiveAuthorizationPolicy", + "plural": "toolhiveauthorizationpolicies", + "group": "toolhive.enterprise.stacklok.com", + "version": "v1alpha1", + "shortNames": [ + "tap", + "authzpolicy" + ], + "scope": "Namespaced", + "description": "ToolhiveAuthorizationPolicy defines authorization policy for an MCP server.", + "references": [ + { + "targetKind": "ClusterPlatformRole", + "paths": [ + "spec.bindings[].roleRef" + ] + } + ], + "referencedBy": [], + "slug": "toolhiveauthorizationpolicy" + } +] diff --git a/static/api-specs/enterprise-crds/platformrolebindings.example.yaml b/static/api-specs/enterprise-crds/platformrolebindings.example.yaml new file mode 100644 index 00000000..f15263ec --- /dev/null +++ b/static/api-specs/enterprise-crds/platformrolebindings.example.yaml @@ -0,0 +1,12 @@ +apiVersion: platform.enterprise.stacklok.com/v1alpha1 +kind: PlatformRoleBinding +metadata: + name: my-platformrolebinding + namespace: default +spec: + bindings: + - from: + - {} + roleRef: + kind: ClusterPlatformRole + name: diff --git a/static/api-specs/enterprise-crds/platformrolebindings.schema.json b/static/api-specs/enterprise-crds/platformrolebindings.schema.json new file mode 100644 index 00000000..253cf372 --- /dev/null +++ b/static/api-specs/enterprise-crds/platformrolebindings.schema.json @@ -0,0 +1,173 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PlatformRoleBinding", + "description": "PlatformRoleBinding binds ClusterPlatformRoles to principals within a namespace.", + "x-kubernetes-group": "platform.enterprise.stacklok.com", + "x-kubernetes-kind": "PlatformRoleBinding", + "x-kubernetes-version": "v1alpha1", + "x-kubernetes-plural": "platformrolebindings", + "x-kubernetes-short-names": [ + "prb", + "platformrolebinding" + ], + "x-kubernetes-scope": "Namespaced", + "properties": { + "spec": { + "description": "PlatformRoleBindingSpec defines the desired state of PlatformRoleBinding.", + "properties": { + "bindings": { + "description": "Bindings is the list of role-to-principal mappings.", + "items": { + "description": "RoleBindingEntry binds a ClusterPlatformRole to a set of principals.", + "properties": { + "from": { + "description": "From is the list of principal conditions that receive the role.", + "items": { + "description": "PrincipalCondition identifies principals by group membership or role assignment.", + "properties": { + "groups": { + "description": "Groups is the list of OIDC groups a principal must belong to.", + "items": { + "maxLength": 253, + "type": "string" + }, + "maxItems": 32, + "type": "array" + }, + "roles": { + "description": "Roles is the list of OIDC roles a principal must have.", + "items": { + "maxLength": 253, + "type": "string" + }, + "maxItems": 32, + "type": "array" + } + }, + "type": "object", + "x-kubernetes-validations": [ + { + "message": "at least one of groups or roles must be non-empty", + "rule": "(has(self.groups) && size(self.groups) > 0) || (has(self.roles) && size(self.roles) > 0)" + } + ] + }, + "maxItems": 32, + "minItems": 1, + "type": "array" + }, + "roleRef": { + "description": "RoleRef references the ClusterPlatformRole to bind.", + "properties": { + "kind": { + "enum": [ + "ClusterPlatformRole" + ], + "type": "string" + }, + "name": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "kind", + "name" + ], + "type": "object" + } + }, + "required": [ + "from", + "roleRef" + ], + "type": "object" + }, + "maxItems": 32, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "bindings" + ], + "type": "object" + }, + "status": { + "description": "PlatformRoleBindingStatus defines the observed state of PlatformRoleBinding.", + "properties": { + "conditions": { + "description": "Conditions represent the latest available observations of the binding's state.", + "items": { + "description": "Condition contains details for one aspect of the current state of this API Resource.", + "properties": { + "lastTransitionTime": { + "description": "lastTransitionTime is the last time the condition transitioned from one status to another.\nThis should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.", + "format": "date-time", + "type": "string" + }, + "message": { + "description": "message is a human readable message indicating details about the transition.\nThis may be an empty string.", + "maxLength": 32768, + "type": "string" + }, + "observedGeneration": { + "description": "observedGeneration represents the .metadata.generation that the condition was set based upon.\nFor instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date\nwith respect to the current state of the instance.", + "format": "int64", + "minimum": 0, + "type": "integer" + }, + "reason": { + "description": "reason contains a programmatic identifier indicating the reason for the condition's last transition.\nProducers of specific condition types may define expected values and meanings for this field,\nand whether the values are considered a guaranteed API.\nThe value should be a CamelCase string.\nThis field may not be empty.", + "maxLength": 1024, + "minLength": 1, + "pattern": "^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$", + "type": "string" + }, + "status": { + "description": "status of the condition, one of True, False, Unknown.", + "enum": [ + "True", + "False", + "Unknown" + ], + "type": "string" + }, + "type": { + "description": "type of condition in CamelCase or in foo.example.com/CamelCase.", + "maxLength": 316, + "pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$", + "type": "string" + } + }, + "required": [ + "lastTransitionTime", + "message", + "reason", + "status", + "type" + ], + "type": "object" + }, + "type": "array", + "x-kubernetes-list-map-keys": [ + "type" + ], + "x-kubernetes-list-type": "map" + }, + "observedGeneration": { + "description": "ObservedGeneration is the most recent generation observed by the controller.", + "format": "int64", + "type": "integer" + }, + "roleCount": { + "description": "RoleCount is the number of role binding entries in Spec.Bindings.", + "format": "int32", + "type": "integer" + } + }, + "type": "object" + } + }, + "type": "object" +} diff --git a/static/api-specs/enterprise-crds/sidebar.json b/static/api-specs/enterprise-crds/sidebar.json new file mode 100644 index 00000000..1675bbdd --- /dev/null +++ b/static/api-specs/enterprise-crds/sidebar.json @@ -0,0 +1,23 @@ +{ + "type": "category", + "label": "Platform CRD reference", + "description": "Reference for Stacklok Enterprise authorization custom resource definitions.", + "link": { + "type": "doc", + "id": "platform/reference/crds/index" + }, + "items": [ + { + "type": "category", + "label": "Enterprise authorization", + "collapsed": false, + "collapsible": false, + "items": [ + "platform/reference/crds/clusterplatformrole", + "platform/reference/crds/clusterplatformrolebinding", + "platform/reference/crds/platformrolebinding", + "platform/reference/crds/toolhiveauthorizationpolicy" + ] + } + ] +} diff --git a/static/api-specs/enterprise-crds/toolhiveauthorizationpolicies.example.yaml b/static/api-specs/enterprise-crds/toolhiveauthorizationpolicies.example.yaml new file mode 100644 index 00000000..c33087bd --- /dev/null +++ b/static/api-specs/enterprise-crds/toolhiveauthorizationpolicies.example.yaml @@ -0,0 +1,8 @@ +apiVersion: toolhive.enterprise.stacklok.com/v1alpha1 +kind: ToolhiveAuthorizationPolicy +metadata: + name: my-toolhiveauthorizationpolicy + namespace: default +spec: + targetRef: + name: diff --git a/static/api-specs/enterprise-crds/toolhiveauthorizationpolicies.schema.json b/static/api-specs/enterprise-crds/toolhiveauthorizationpolicies.schema.json new file mode 100644 index 00000000..f323c417 --- /dev/null +++ b/static/api-specs/enterprise-crds/toolhiveauthorizationpolicies.schema.json @@ -0,0 +1,318 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ToolhiveAuthorizationPolicy", + "description": "ToolhiveAuthorizationPolicy defines authorization policy for an MCP server.", + "x-kubernetes-group": "toolhive.enterprise.stacklok.com", + "x-kubernetes-kind": "ToolhiveAuthorizationPolicy", + "x-kubernetes-version": "v1alpha1", + "x-kubernetes-plural": "toolhiveauthorizationpolicies", + "x-kubernetes-short-names": [ + "tap", + "authzpolicy" + ], + "x-kubernetes-scope": "Namespaced", + "properties": { + "spec": { + "description": "ToolhiveAuthorizationPolicySpec defines the desired state of ToolhiveAuthorizationPolicy.", + "properties": { + "bindings": { + "description": "Bindings is the list of role-to-principal bindings for this policy.", + "items": { + "description": "PolicyBinding binds a ClusterPlatformRole within the scope of this policy.", + "properties": { + "from": { + "description": "From optionally narrows this binding to a subset of principals.\nWhen omitted, the binding applies to every principal granted this role\nvia the cluster-scoped binding CRD.", + "items": { + "description": "PrincipalCondition identifies principals by group membership or role assignment.", + "properties": { + "groups": { + "description": "Groups is the list of OIDC groups a principal must belong to.", + "items": { + "maxLength": 253, + "type": "string" + }, + "maxItems": 32, + "type": "array" + }, + "roles": { + "description": "Roles is the list of OIDC roles a principal must have.", + "items": { + "maxLength": 253, + "type": "string" + }, + "maxItems": 32, + "type": "array" + } + }, + "type": "object", + "x-kubernetes-validations": [ + { + "message": "at least one of groups or roles must be non-empty", + "rule": "(has(self.groups) && size(self.groups) > 0) || (has(self.roles) && size(self.roles) > 0)" + } + ] + }, + "maxItems": 16, + "type": "array" + }, + "roleRef": { + "description": "RoleRef references the ClusterPlatformRole to bind.", + "properties": { + "kind": { + "enum": [ + "ClusterPlatformRole" + ], + "type": "string" + }, + "name": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "kind", + "name" + ], + "type": "object" + }, + "ruleRestrictions": { + "description": "RuleRestrictions optionally narrows which MCP resources this binding applies to.\nEach item scopes to a specific resource type; items are unioned.\nWhen omitted, the binding applies to all resources permitted by the role.", + "items": { + "description": "RuleRestriction narrows a binding to specific MCP resource names of one or more types.", + "properties": { + "prompts": { + "description": "Prompts is the list of MCP prompt names to restrict to.", + "items": { + "maxLength": 253, + "type": "string" + }, + "maxItems": 64, + "type": "array" + }, + "resources": { + "description": "Resources is the list of MCP resource URIs to restrict to.", + "items": { + "maxLength": 253, + "type": "string" + }, + "maxItems": 64, + "type": "array" + }, + "tools": { + "description": "Tools is the list of MCP tool names to restrict to.", + "items": { + "maxLength": 253, + "type": "string" + }, + "maxItems": 64, + "type": "array" + } + }, + "type": "object" + }, + "maxItems": 16, + "type": "array" + }, + "toolHintFilter": { + "description": "ToolHintFilter optionally gates call_tool on MCP tool annotation hints.\nEach non-nil filter field adds a Cedar when condition; multiple fields are ANDed.", + "properties": { + "destructiveHint": { + "description": "DestructiveHint restricts call_tool to tools with destructiveHint matching this value.", + "type": "boolean" + }, + "readOnlyHint": { + "description": "ReadOnlyHint restricts call_tool to tools with readOnlyHint matching this value.", + "type": "boolean" + } + }, + "type": "object" + } + }, + "required": [ + "roleRef" + ], + "type": "object" + }, + "maxItems": 128, + "type": "array" + }, + "deny": { + "description": "Deny is the list of explicit deny rules. Each rule compiles to a Cedar\nforbid statement. In Cedar's evaluation model, forbid unconditionally\noverrides permit regardless of declaration order — a matching deny vetoes\nany permit that would otherwise grant access.", + "items": { + "description": "DenyRule explicitly denies a set of MCP actions, optionally scoped to specific resources.", + "properties": { + "actions": { + "description": "Actions is the list of MCP actions to deny.", + "items": { + "description": "DenyAction is an MCP action that can be denied by a deny rule (wildcard excluded).", + "enum": [ + "call_tool", + "list_tools", + "get_prompt", + "list_prompts", + "read_resource", + "list_resources" + ], + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "prompts": { + "description": "Prompts optionally scopes the deny rule to specific MCP prompts.", + "items": { + "type": "string" + }, + "maxItems": 64, + "type": "array" + }, + "resources": { + "description": "Resources optionally scopes the deny rule to specific MCP resource URIs.", + "items": { + "type": "string" + }, + "maxItems": 64, + "type": "array" + }, + "tools": { + "description": "Tools optionally scopes the deny rule to specific MCP tools.\nWhen omitted alongside Prompts and Resources, the deny applies to all\nresources on the server.", + "items": { + "type": "string" + }, + "maxItems": 64, + "type": "array" + } + }, + "required": [ + "actions" + ], + "type": "object" + }, + "type": "array" + }, + "targetRef": { + "description": "TargetRef identifies the MCP server this policy applies to.", + "properties": { + "apiGroup": { + "default": "toolhive.stacklok.dev", + "description": "APIGroup is the API group of the target resource. Defaults to the\nOSS toolhive group; carried explicitly so future kinds can be\nadded without a breaking change.", + "type": "string" + }, + "kind": { + "default": "MCPServer", + "description": "Kind is the kind of the target resource. Defaults to MCPServer.", + "enum": [ + "MCPServer", + "MCPRemoteProxy" + ], + "type": "string" + }, + "name": { + "description": "Name is the name of the target resource.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "required": [ + "targetRef" + ], + "type": "object", + "x-kubernetes-validations": [ + { + "message": "at least one of bindings or deny must be non-empty", + "rule": "(has(self.bindings) && self.bindings.size() > 0) || (has(self.deny) && self.deny.size() > 0)" + } + ] + }, + "status": { + "description": "ToolhiveAuthorizationPolicyStatus defines the observed state of ToolhiveAuthorizationPolicy.", + "properties": { + "bindingCount": { + "description": "BindingCount is the number of role bindings in the policy spec.\nPopulated by the controller during reconciliation.", + "format": "int32", + "type": "integer" + }, + "compiledConfigMap": { + "description": "CompiledConfigMap is the name of the ConfigMap that holds the compiled policy.", + "type": "string" + }, + "conditions": { + "description": "Conditions represent the latest available observations of the policy's state.", + "items": { + "description": "Condition contains details for one aspect of the current state of this API Resource.", + "properties": { + "lastTransitionTime": { + "description": "lastTransitionTime is the last time the condition transitioned from one status to another.\nThis should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.", + "format": "date-time", + "type": "string" + }, + "message": { + "description": "message is a human readable message indicating details about the transition.\nThis may be an empty string.", + "maxLength": 32768, + "type": "string" + }, + "observedGeneration": { + "description": "observedGeneration represents the .metadata.generation that the condition was set based upon.\nFor instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date\nwith respect to the current state of the instance.", + "format": "int64", + "minimum": 0, + "type": "integer" + }, + "reason": { + "description": "reason contains a programmatic identifier indicating the reason for the condition's last transition.\nProducers of specific condition types may define expected values and meanings for this field,\nand whether the values are considered a guaranteed API.\nThe value should be a CamelCase string.\nThis field may not be empty.", + "maxLength": 1024, + "minLength": 1, + "pattern": "^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$", + "type": "string" + }, + "status": { + "description": "status of the condition, one of True, False, Unknown.", + "enum": [ + "True", + "False", + "Unknown" + ], + "type": "string" + }, + "type": { + "description": "type of condition in CamelCase or in foo.example.com/CamelCase.", + "maxLength": 316, + "pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$", + "type": "string" + } + }, + "required": [ + "lastTransitionTime", + "message", + "reason", + "status", + "type" + ], + "type": "object" + }, + "type": "array", + "x-kubernetes-list-map-keys": [ + "type" + ], + "x-kubernetes-list-type": "map" + }, + "denyCount": { + "description": "DenyCount is the number of deny rules in the policy spec.\nPopulated by the controller during reconciliation.", + "format": "int32", + "type": "integer" + }, + "observedGeneration": { + "description": "ObservedGeneration is the metadata.generation last reconciled.", + "format": "int64", + "type": "integer" + } + }, + "type": "object" + } + }, + "type": "object" +} diff --git a/static/api-specs/crds/embeddingservers.example.yaml b/static/api-specs/toolhive-crds/embeddingservers.example.yaml similarity index 100% rename from static/api-specs/crds/embeddingservers.example.yaml rename to static/api-specs/toolhive-crds/embeddingservers.example.yaml diff --git a/static/api-specs/crds/embeddingservers.schema.json b/static/api-specs/toolhive-crds/embeddingservers.schema.json similarity index 100% rename from static/api-specs/crds/embeddingservers.schema.json rename to static/api-specs/toolhive-crds/embeddingservers.schema.json diff --git a/static/api-specs/crds/index.json b/static/api-specs/toolhive-crds/index.json similarity index 100% rename from static/api-specs/crds/index.json rename to static/api-specs/toolhive-crds/index.json diff --git a/static/api-specs/crds/mcpauthzconfigs.example.yaml b/static/api-specs/toolhive-crds/mcpauthzconfigs.example.yaml similarity index 100% rename from static/api-specs/crds/mcpauthzconfigs.example.yaml rename to static/api-specs/toolhive-crds/mcpauthzconfigs.example.yaml diff --git a/static/api-specs/crds/mcpauthzconfigs.schema.json b/static/api-specs/toolhive-crds/mcpauthzconfigs.schema.json similarity index 100% rename from static/api-specs/crds/mcpauthzconfigs.schema.json rename to static/api-specs/toolhive-crds/mcpauthzconfigs.schema.json diff --git a/static/api-specs/crds/mcpexternalauthconfigs.example.yaml b/static/api-specs/toolhive-crds/mcpexternalauthconfigs.example.yaml similarity index 100% rename from static/api-specs/crds/mcpexternalauthconfigs.example.yaml rename to static/api-specs/toolhive-crds/mcpexternalauthconfigs.example.yaml diff --git a/static/api-specs/crds/mcpexternalauthconfigs.schema.json b/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json similarity index 100% rename from static/api-specs/crds/mcpexternalauthconfigs.schema.json rename to static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json diff --git a/static/api-specs/crds/mcpgroups.example.yaml b/static/api-specs/toolhive-crds/mcpgroups.example.yaml similarity index 100% rename from static/api-specs/crds/mcpgroups.example.yaml rename to static/api-specs/toolhive-crds/mcpgroups.example.yaml diff --git a/static/api-specs/crds/mcpgroups.schema.json b/static/api-specs/toolhive-crds/mcpgroups.schema.json similarity index 100% rename from static/api-specs/crds/mcpgroups.schema.json rename to static/api-specs/toolhive-crds/mcpgroups.schema.json diff --git a/static/api-specs/crds/mcpoidcconfigs.example.yaml b/static/api-specs/toolhive-crds/mcpoidcconfigs.example.yaml similarity index 100% rename from static/api-specs/crds/mcpoidcconfigs.example.yaml rename to static/api-specs/toolhive-crds/mcpoidcconfigs.example.yaml diff --git a/static/api-specs/crds/mcpoidcconfigs.schema.json b/static/api-specs/toolhive-crds/mcpoidcconfigs.schema.json similarity index 100% rename from static/api-specs/crds/mcpoidcconfigs.schema.json rename to static/api-specs/toolhive-crds/mcpoidcconfigs.schema.json diff --git a/static/api-specs/crds/mcpregistries.example.yaml b/static/api-specs/toolhive-crds/mcpregistries.example.yaml similarity index 100% rename from static/api-specs/crds/mcpregistries.example.yaml rename to static/api-specs/toolhive-crds/mcpregistries.example.yaml diff --git a/static/api-specs/crds/mcpregistries.schema.json b/static/api-specs/toolhive-crds/mcpregistries.schema.json similarity index 100% rename from static/api-specs/crds/mcpregistries.schema.json rename to static/api-specs/toolhive-crds/mcpregistries.schema.json diff --git a/static/api-specs/crds/mcpremoteproxies.example.yaml b/static/api-specs/toolhive-crds/mcpremoteproxies.example.yaml similarity index 100% rename from static/api-specs/crds/mcpremoteproxies.example.yaml rename to static/api-specs/toolhive-crds/mcpremoteproxies.example.yaml diff --git a/static/api-specs/crds/mcpremoteproxies.schema.json b/static/api-specs/toolhive-crds/mcpremoteproxies.schema.json similarity index 100% rename from static/api-specs/crds/mcpremoteproxies.schema.json rename to static/api-specs/toolhive-crds/mcpremoteproxies.schema.json diff --git a/static/api-specs/crds/mcpserverentries.example.yaml b/static/api-specs/toolhive-crds/mcpserverentries.example.yaml similarity index 100% rename from static/api-specs/crds/mcpserverentries.example.yaml rename to static/api-specs/toolhive-crds/mcpserverentries.example.yaml diff --git a/static/api-specs/crds/mcpserverentries.schema.json b/static/api-specs/toolhive-crds/mcpserverentries.schema.json similarity index 100% rename from static/api-specs/crds/mcpserverentries.schema.json rename to static/api-specs/toolhive-crds/mcpserverentries.schema.json diff --git a/static/api-specs/crds/mcpservers.example.yaml b/static/api-specs/toolhive-crds/mcpservers.example.yaml similarity index 100% rename from static/api-specs/crds/mcpservers.example.yaml rename to static/api-specs/toolhive-crds/mcpservers.example.yaml diff --git a/static/api-specs/crds/mcpservers.schema.json b/static/api-specs/toolhive-crds/mcpservers.schema.json similarity index 100% rename from static/api-specs/crds/mcpservers.schema.json rename to static/api-specs/toolhive-crds/mcpservers.schema.json diff --git a/static/api-specs/crds/mcptelemetryconfigs.example.yaml b/static/api-specs/toolhive-crds/mcptelemetryconfigs.example.yaml similarity index 100% rename from static/api-specs/crds/mcptelemetryconfigs.example.yaml rename to static/api-specs/toolhive-crds/mcptelemetryconfigs.example.yaml diff --git a/static/api-specs/crds/mcptelemetryconfigs.schema.json b/static/api-specs/toolhive-crds/mcptelemetryconfigs.schema.json similarity index 100% rename from static/api-specs/crds/mcptelemetryconfigs.schema.json rename to static/api-specs/toolhive-crds/mcptelemetryconfigs.schema.json diff --git a/static/api-specs/crds/mcptoolconfigs.example.yaml b/static/api-specs/toolhive-crds/mcptoolconfigs.example.yaml similarity index 100% rename from static/api-specs/crds/mcptoolconfigs.example.yaml rename to static/api-specs/toolhive-crds/mcptoolconfigs.example.yaml diff --git a/static/api-specs/crds/mcptoolconfigs.schema.json b/static/api-specs/toolhive-crds/mcptoolconfigs.schema.json similarity index 100% rename from static/api-specs/crds/mcptoolconfigs.schema.json rename to static/api-specs/toolhive-crds/mcptoolconfigs.schema.json diff --git a/static/api-specs/crds/mcpwebhookconfigs.example.yaml b/static/api-specs/toolhive-crds/mcpwebhookconfigs.example.yaml similarity index 100% rename from static/api-specs/crds/mcpwebhookconfigs.example.yaml rename to static/api-specs/toolhive-crds/mcpwebhookconfigs.example.yaml diff --git a/static/api-specs/crds/mcpwebhookconfigs.schema.json b/static/api-specs/toolhive-crds/mcpwebhookconfigs.schema.json similarity index 100% rename from static/api-specs/crds/mcpwebhookconfigs.schema.json rename to static/api-specs/toolhive-crds/mcpwebhookconfigs.schema.json diff --git a/static/api-specs/crds/sidebar.json b/static/api-specs/toolhive-crds/sidebar.json similarity index 89% rename from static/api-specs/crds/sidebar.json rename to static/api-specs/toolhive-crds/sidebar.json index 07d78024..470aceb2 100644 --- a/static/api-specs/crds/sidebar.json +++ b/static/api-specs/toolhive-crds/sidebar.json @@ -1,7 +1,7 @@ { "type": "category", - "label": "CRD reference", - "description": "Reference for the Kubernetes custom resources managed by the ToolHive operator", + "label": "Kubernetes CRD reference", + "description": "Reference for all ToolHive Kubernetes Operator custom resource definitions.", "link": { "type": "doc", "id": "toolhive/reference/crds/index" diff --git a/static/api-specs/crds/virtualmcpcompositetooldefinitions.example.yaml b/static/api-specs/toolhive-crds/virtualmcpcompositetooldefinitions.example.yaml similarity index 100% rename from static/api-specs/crds/virtualmcpcompositetooldefinitions.example.yaml rename to static/api-specs/toolhive-crds/virtualmcpcompositetooldefinitions.example.yaml diff --git a/static/api-specs/crds/virtualmcpcompositetooldefinitions.schema.json b/static/api-specs/toolhive-crds/virtualmcpcompositetooldefinitions.schema.json similarity index 100% rename from static/api-specs/crds/virtualmcpcompositetooldefinitions.schema.json rename to static/api-specs/toolhive-crds/virtualmcpcompositetooldefinitions.schema.json diff --git a/static/api-specs/crds/virtualmcpservers.example.yaml b/static/api-specs/toolhive-crds/virtualmcpservers.example.yaml similarity index 100% rename from static/api-specs/crds/virtualmcpservers.example.yaml rename to static/api-specs/toolhive-crds/virtualmcpservers.example.yaml diff --git a/static/api-specs/crds/virtualmcpservers.schema.json b/static/api-specs/toolhive-crds/virtualmcpservers.schema.json similarity index 100% rename from static/api-specs/crds/virtualmcpservers.schema.json rename to static/api-specs/toolhive-crds/virtualmcpservers.schema.json diff --git a/static/img/icons/stacklok-website-icons-circuit-dark-green.svg b/static/img/icons/stacklok-website-icons-circuit-dark-green.svg new file mode 100644 index 00000000..e23e0fb7 --- /dev/null +++ b/static/img/icons/stacklok-website-icons-circuit-dark-green.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/img/icons/stacklok-website-icons-circuit-light-green.svg b/static/img/icons/stacklok-website-icons-circuit-light-green.svg new file mode 100644 index 00000000..6e016e51 --- /dev/null +++ b/static/img/icons/stacklok-website-icons-circuit-light-green.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/img/mcp-servers/stacklok-website-icons-efficiency-dark-green.svg b/static/img/icons/stacklok-website-icons-efficiency-dark-green.svg similarity index 100% rename from static/img/mcp-servers/stacklok-website-icons-efficiency-dark-green.svg rename to static/img/icons/stacklok-website-icons-efficiency-dark-green.svg diff --git a/static/img/mcp-servers/stacklok-website-icons-efficiency-light-green.svg b/static/img/icons/stacklok-website-icons-efficiency-light-green.svg similarity index 100% rename from static/img/mcp-servers/stacklok-website-icons-efficiency-light-green.svg rename to static/img/icons/stacklok-website-icons-efficiency-light-green.svg diff --git a/static/img/icons/stacklok-website-icons-security-dark-green.svg b/static/img/icons/stacklok-website-icons-security-dark-green.svg new file mode 100644 index 00000000..5472cd49 --- /dev/null +++ b/static/img/icons/stacklok-website-icons-security-dark-green.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/img/icons/stacklok-website-icons-security-light-green.svg b/static/img/icons/stacklok-website-icons-security-light-green.svg new file mode 100644 index 00000000..13b000ca --- /dev/null +++ b/static/img/icons/stacklok-website-icons-security-light-green.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vercel.json b/vercel.json index 1ae0ef30..81d293e1 100644 --- a/vercel.json +++ b/vercel.json @@ -1,6 +1,11 @@ { "$schema": "https://openapi.vercel.sh/vercel.json", "redirects": [ + { + "source": "/toolhive/enterprise", + "destination": "/platform", + "permanent": true + }, { "source": "/toolhive/quickstart", "destination": "/toolhive/guides-cli/quickstart", From 9fb1b18202447841de0db4c284a02cf8d203a88b Mon Sep 17 00:00:00 2001 From: Dan Barr Date: Wed, 24 Jun 2026 15:32:17 -0400 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- AGENTS.md | 2 +- STYLE-GUIDE.md | 4 ++-- docs/platform/enterprise-manager/policies/index.mdx | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b62ca3be..e99146db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -304,7 +304,7 @@ Stacklok Enterprise includes turnkey integrations for common identity providers. ```ts title="sidebars.ts" { type: 'doc', - id: 'toolhive/enterprise-manager/config-server', + id: 'platform/enterprise-manager/index', className: 'enterprise-only', } ``` diff --git a/STYLE-GUIDE.md b/STYLE-GUIDE.md index 08d1c18a..3f03e62c 100644 --- a/STYLE-GUIDE.md +++ b/STYLE-GUIDE.md @@ -323,7 +323,7 @@ Specific guidelines for Docusaurus: These are the projects we work on, and a short description of each one. -**Stacklok Enterprise**: Stacklok’s commercial offering for enterprises: the +**Stacklok Enterprise**: Stacklok's commercial offering for enterprises: the enterprise-licensed distribution of ToolHive. It adds turnkey identity provider integration, centralized policy enforcement, hardened and signed releases, and SLA-backed support, plus commercial support terms, services hours from the @@ -332,7 +332,7 @@ server images. **ToolHive**: A collection of open source projects that form the foundation of Stacklok Enterprise. ToolHive includes everything you need to use MCP servers in -production. It’s made up of four key components: the Runtime, Registry Server, +production. It's made up of four key components: the Runtime, Registry Server, Gateway, and Portal. It's written bi-capitalized as one word (not "Toolhive" or "Tool Hive"). diff --git a/docs/platform/enterprise-manager/policies/index.mdx b/docs/platform/enterprise-manager/policies/index.mdx index 5cb89615..b81bce59 100644 --- a/docs/platform/enterprise-manager/policies/index.mdx +++ b/docs/platform/enterprise-manager/policies/index.mdx @@ -22,8 +22,7 @@ flows, and what parts of the Stacklok Desktop are visible. | [Stacklok Desktop](./desktop-app.mdx) | Show or hide the Playground tab and help menu | | [AI assistant](../../enterprise-cloud-ui/configure.mdx#feature-flags) | Show or hide the AI assistant in the Cloud UI | -Advanced directives — such as LLM Gateway configuration — are not covered in -these guides. +Advanced directives, such as LLM Gateway configuration, are not covered in these guides. ## Enforcement levels From 7bbb4f45b50f1411005c54a00d21d153fd867b01 Mon Sep 17 00:00:00 2001 From: Dan Barr <6922515+danbarr@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:36:53 -0400 Subject: [PATCH 3/3] Fix formatting Signed-off-by: Dan Barr <6922515+danbarr@users.noreply.github.com> --- docs/platform/enterprise-manager/policies/index.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/platform/enterprise-manager/policies/index.mdx b/docs/platform/enterprise-manager/policies/index.mdx index b81bce59..ed4eb482 100644 --- a/docs/platform/enterprise-manager/policies/index.mdx +++ b/docs/platform/enterprise-manager/policies/index.mdx @@ -22,7 +22,8 @@ flows, and what parts of the Stacklok Desktop are visible. | [Stacklok Desktop](./desktop-app.mdx) | Show or hide the Playground tab and help menu | | [AI assistant](../../enterprise-cloud-ui/configure.mdx#feature-flags) | Show or hide the AI assistant in the Cloud UI | -Advanced directives, such as LLM Gateway configuration, are not covered in these guides. +Advanced directives, such as LLM Gateway configuration, are not covered in these +guides. ## Enforcement levels