Skip to content

Latest commit

 

History

History
726 lines (517 loc) · 75.1 KB

File metadata and controls

726 lines (517 loc) · 75.1 KB

Administrator Guide

This guide covers deploying Notebook Intelligence at scale — JupyterHub, KubeSpawner, Kubeflow, multi-tenant clusters, regulated environments. For end-user documentation, see the README.

NBI is a per-user tool. Every section below assumes the extension runs inside a per-user Jupyter Server, not a shared one. Server-side state is per-user; there is no central NBI service.


Table of contents


Install layout and config precedence

NBI reads configuration from three layers, listed in order of precedence (later wins):

  1. Environment-wide base config<env-prefix>/share/jupyter/nbi/config.json and <env-prefix>/share/jupyter/nbi/mcp.json. Bake into your image. Read once at startup.
  2. User config~/.jupyter/nbi/config.json and ~/.jupyter/nbi/mcp.json. The user mutates these via the Settings dialog. Lives on the per-user PVC.
  3. Environment variablesNBI_* and certain provider variables (see the reference table). Override at pod startup time.

Traitlets configured via JupyterLab CLI flags or jupyter_server_config.py (e.g., c.NotebookIntelligence.disabled_providers = [...]) are evaluated at server startup. Most env-var overrides (NBI_*_POLICY, NBI_ALLOW_GITHUB_*, NBI_*_MANAGEMENT_POLICY, etc.) are also resolved once at startup and cached on the handler classes — flipping them requires a JupyterLab restart. The NBI_ENABLED_PROVIDERS and NBI_ENABLED_BUILTIN_TOOLS re-enable env vars (gated by allow_enabling_*_with_env) are the exception: those are read on every request.

Manual edits to config.json while JupyterLab is running require a JupyterLab restart to take effect. Edits via the Settings dialog are picked up live.


Persistent-volume layout

Path Persist? Notes
~/.jupyter/nbi/config.json Yes User's chosen provider, models, MCP servers, plus plaintext API keys. Treat as a secret.
~/.jupyter/nbi/user-data.json Yes Encrypted GitHub Copilot access token, written when "remember login" is enabled. Encrypted with NBI_GH_ACCESS_TOKEN_PASSWORD.
~/.jupyter/nbi/rules/ Yes User's ruleset markdown files.
~/.jupyter/nbi/mcp.json Yes User's MCP server config (alternative to managing via the Settings dialog).
~/.claude/skills/ Yes User-scope Claude skills (including managed skills).
~/.claude/projects/ Yes Claude Code session transcripts. Required for "Resume previous Claude session". Managed by Claude CLI, not NBI.
<env-prefix>/share/jupyter/nbi/ No (image) Org-wide base config. Bake into your container image.
Project-scope <project>/.claude/skills/ Per project Lives in the user's working directory. Persists if the working directory does.

For Kubeflow or KubeSpawner: mount the user's home directory on a PVC and ensure ~/.jupyter and ~/.claude are inside that mount. Anything else (/tmp, ~/.cache) can be ephemeral.


Shared filesystem and multi-user notes

If users share a home directory across nodes (NFS-backed shared HPC, classroom labs):

  • Race conditions in ~/.jupyter/nbi/. Concurrent writes from two login nodes can corrupt config.json. NBI does not file-lock. Pin each user to one node, or use a per-node config prefix.
  • NBI_GH_ACCESS_TOKEN_PASSWORD default is unsafe. The default password (nbi-access-token-password) is shared across installs. On a multi-tenant cluster, anyone with read access to another user's ~/.jupyter/nbi/user-data.json can decrypt their Copilot token. Set a per-user password (e.g., derived from the Hub user secret), or disable "remember login" entirely (see Restricting features).
  • Skill collisions. Two users sharing ~/.claude/skills/ will see each other's skills. Make sure each user has a unique home.

Environment variables and traitlets

The full surface, in one table.

Name Type Default Source Purpose
disabled_providers List [] traitlet on NotebookIntelligence Hide providers from the user dropdown. Values: github-copilot, ollama, litellm-compatible, openai-compatible.
allow_enabling_providers_with_env Bool False traitlet If true, NBI_ENABLED_PROVIDERS re-enables hidden providers per pod.
NBI_ENABLED_PROVIDERS csv unset env Comma-separated provider IDs to re-enable. Effective only when allow_enabling_providers_with_env=True.
disabled_tools List [] traitlet Hide built-in tools from agent mode. Values listed in Restricting features.
allow_enabling_tools_with_env Bool False traitlet If true, NBI_ENABLED_BUILTIN_TOOLS re-enables hidden tools per pod.
NBI_ENABLED_BUILTIN_TOOLS csv unset env Comma-separated tool IDs to re-enable. Effective only when allow_enabling_tools_with_env=True.
disabled_coding_agent_launchers List [] traitlet Hide JupyterLab launcher tiles for coding-agent CLIs even when the CLI is on PATH. Valid IDs: claude-code, opencode, pi, github-copilot-cli, codex. See Disabling coding-agent launcher tiles.
allow_enabling_coding_agent_launchers_with_env Bool False traitlet If true, NBI_ENABLED_CODING_AGENT_LAUNCHERS re-enables hidden tiles per pod.
NBI_ENABLED_CODING_AGENT_LAUNCHERS csv unset env Comma-separated launcher IDs to re-enable. Effective only when allow_enabling_coding_agent_launchers_with_env=True.
enable_chat_feedback Bool False traitlet Enables thumbs-up/down UI in chat and emits in-process telemetry events.
additional_skipped_workspace_directories List [] traitlet Extra directory names to skip in the chat-sidebar @-mention workspace file picker. Merged with the built-in skips (__pycache__, node_modules). Match is by directory name only, case-sensitive.
NBI_ADDITIONAL_SKIPPED_WORKSPACE_DIRECTORIES csv unset env (appends to traitlet) Comma-separated extra directory names. Resolved at server startup and concatenated with the traitlet value, so a spawn profile can add to (rather than replace) the org-wide list.
allow_github_skill_import Bool True traitlet When False, hides the Import from GitHub button in the Skills panel and rejects /skills/import POSTs with 403. Does not affect the managed-skills reconciler.
NBI_ALLOW_GITHUB_SKILL_IMPORT bool unset env (overrides traitlet) Per-pod override for allow_github_skill_import. Accepts true/false/1/0/yes/no/on/off (case-insensitive). Useful for varying the policy across spawn profiles.
skills_manifest str "" traitlet URL or filesystem path to a managed-skills manifest, or a comma-separated list of either. Manifests are unioned with first-wins URL dedupe; name collisions surface as per-entry errors. See docs/skills.md.
NBI_SKILLS_MANIFEST str unset env (overrides traitlet) Same as above; env takes precedence.
skills_manifest_interval int 86400 traitlet Seconds between reconciles.
NBI_SKILLS_MANIFEST_INTERVAL int unset env (overrides traitlet) Same as above; env takes precedence.
managed_skills_token str "" traitlet Bearer token for managed-skills GitHub fetches.
NBI_MANAGED_SKILLS_TOKEN str unset env (overrides traitlet) Same as above; env takes precedence.
allow_github_plugin_import Bool True traitlet When False, hides the "From GitHub" affordance in the Plugins panel and rejects claude plugin marketplace add requests whose source resolves as a GitHub URL or owner/repo shorthand. Local-path and arbitrary-URL sources remain available.
NBI_ALLOW_GITHUB_PLUGIN_IMPORT bool unset env (overrides traitlet) Per-pod override for allow_github_plugin_import. Accepts true/false/1/0/yes/no/on/off (case-insensitive).
skill_max_archive_mb Int 100 traitlet Per-archive on-wire size cap (megabytes) for skill bundles fetched from GitHub. Applies to both user imports and managed-skills tarballs. 0 disables the cap.
NBI_SKILL_MAX_ARCHIVE_MB int unset env (overrides traitlet) Same as above; env takes precedence.
upload_max_mb Int 50 traitlet Per-file size cap (megabytes) for the shared upload endpoint used by chat-sidebar attachments and terminal drag-drop. Requests over the cap return HTTP 413. 0 disables the cap.
NBI_UPLOAD_MAX_MB int unset env (overrides traitlet) Same as above; env takes precedence.
upload_retention_hours Int 24 traitlet How long staged uploads survive in the temp directory before the next upload sweeps them. 0 keeps only the atexit purge (uploads survive the session).
NBI_UPLOAD_RETENTION_HOURS int unset env (overrides traitlet) Same as above; env takes precedence.
tour_config_path str "" traitlet Filesystem path to a YAML/JSON file with admin overrides for the first-run sidebar tour copy. See docs/admin-tour-config.md.
NBI_TOUR_CONFIG_PATH str unset env (overrides traitlet) Same as above; env takes precedence.
NBI_GH_ACCESS_TOKEN_PASSWORD str nbi-access-token-password env Password used to encrypt the stored Copilot token in user-data.json. Change in multi-tenant deployments.
NBI_RULES_AUTO_RELOAD bool true env When false, ruleset edits require a JupyterLab restart to take effect.
NBI_CLAUDE_CLI_PATH str unset env Absolute path to the Claude Code CLI binary. When unset, NBI looks up claude on PATH.
NBI_OPENCODE_CLI_PATH str unset env Absolute path to the opencode CLI. When unset, NBI looks up opencode on PATH. Gates the opencode launcher tile.
NBI_PI_CLI_PATH str unset env Absolute path to the Pi CLI. When unset, NBI looks up pi on PATH. Gates the Pi launcher tile.
NBI_GITHUB_COPILOT_CLI_PATH str unset env Absolute path to the GitHub Copilot CLI. When unset, NBI looks up copilot on PATH. Gates the GitHub Copilot launcher tile.
NBI_CODEX_CLI_PATH str unset env Absolute path to the OpenAI Codex CLI. When unset, NBI looks up codex on PATH. Gates the Codex launcher tile.
NBI_GHE_SUBDOMAIN str "" env GitHub Enterprise subdomain for GitHub Copilot users on a GHE tenant. Empty selects github.com.
NBI_GITHUB_ENTERPRISE_HOSTS csv "" env Comma-separated hostnames the plugin marketplace detector treats as GitHub. Cookie-domain shape: bare token (github.acme.com) matches exactly; leading-dot token (.acme.com) matches any subdomain of acme.com. Independent of NBI_GHE_SUBDOMAIN, which only configures the Copilot OAuth tenant. Required so allow_github_plugin_import = False actually gates GHE marketplace adds and so the GITHUB_TOKEN / gh auth token chain injects on GHE sources.
NBI_LOG_LEVEL str INFO env Python logging level for the notebook_intelligence logger.
NBI_DISABLE_OUTPUT_SCRUB bool unset env When set (1 / true / yes / on), disables the shell-tool output scrubber so raw stdout/stderr (including any env-var values that leak) is sent through to chat. Default off; the scrubber redacts values for sensitive-named env vars (TOKEN, SECRET, API_KEY, ...) plus tokens with well-known credential prefixes (ghp_, sk-ant-, AKIA, ...). Opt out only when debugging credential helpers where the redaction interferes.
GITHUB_TOKEN, GH_TOKEN str unset env Used (in that order) by user-initiated skill imports and GitHub-sourced plugin marketplace adds for GitHub auth. Falls back to gh CLI auth.
NBI_*_POLICY str user-choice env Lock individual Settings panel toggles. See README → Admin policies for the full list of *_POLICY env vars and matching traitlets, including NBI_SKILLS_MANAGEMENT_POLICY, NBI_CLAUDE_MCP_MANAGEMENT_POLICY, NBI_CLAUDE_PLUGINS_MANAGEMENT_POLICY, NBI_TERMINAL_DRAG_DROP_POLICY, and NBI_REFRESH_OPEN_FILES_ON_DISK_CHANGE_POLICY.

Configure traitlets in jupyter_server_config.py:

c.NotebookIntelligence.disabled_providers = ["openai-compatible", "litellm-compatible"]
c.NotebookIntelligence.allow_enabling_providers_with_env = True
c.NotebookIntelligence.disabled_tools = ["nbi-command-execute"]
c.NotebookIntelligence.skills_manifest = "https://internal.example.com/manifests/data-science-team.yaml"

Security model

NBI runs entirely inside the user's Jupyter Server process. There is no privilege boundary between NBI and the user. In particular:

  • Built-in tools execute as the user.
    • nbi-command-execute runs arbitrary shell commands.
    • nbi-file-edit and nbi-file-read read and write any file the user can.
    • nbi-notebook-edit and nbi-notebook-execute modify and run notebooks.
  • MCP stdio servers are launched as user subprocesses with the user's environment. NBI does not sandbox them.
  • Claude Code CLI inherits the user's environment, including filesystem permissions and any auth tokens in ~/.claude/.

For regulated tenants:

  1. Disable the most powerful tools — at minimum nbi-command-execute and nbi-file-edit. See Restricting features.
  2. Restrict the providers the user can pick. Force a single self-hosted endpoint with disabled_providers plus the org's base config.
  3. Disable user-initiated skill imports with allow_github_skill_import = False (env NBI_ALLOW_GITHUB_SKILL_IMPORT=false), and plugin marketplace adds from GitHub with allow_github_plugin_import = False (env NBI_ALLOW_GITHUB_PLUGIN_IMPORT=false). Reinforce at the network layer where stronger isolation is required. See skills.md and the Plugins tab section below.
  4. Run with a non-root container user, with no host-network access and no host-path mounts beyond the user's PVC.

API-key handling

By default, custom-provider API keys (Anthropic, OpenAI-compatible, LiteLLM-compatible) are stored plaintext in ~/.jupyter/nbi/config.json. This is acceptable for single-tenant developer workstations and unacceptable for multi-tenant clusters.

Recommended approach for clusters:

  • Inject the org's keys via env vars at pod startup. Set the provider's expected env var (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) on the pod. Configure the provider in <env-prefix>/share/jupyter/nbi/config.json without a key — NBI picks up the provider's standard env var.
  • Source secrets from your secret manager. Vault, External Secrets Operator, AWS Secrets Manager, GCP Secret Manager, or KubeSpawner's c.KubeSpawner.environment callback can all populate the pod env from a secret backend at spawn time.
  • Don't commit config.json. Even the env-prefix base config should not contain keys; pull keys from env at spawn.

${ENV_VAR}-style interpolation inside config.json is not currently supported. Tracked as a feature request.


Self-hosted LLM endpoints

NBI's openai-compatible and litellm-compatible providers can target any endpoint that speaks the respective wire format.

Azure OpenAI (via the openai-compatible provider):

{
  "providers": {
    "openai-compatible": {
      "base_url": "https://my-resource.openai.azure.com/openai/deployments/gpt-4-deployment",
      "api_key": "${AZURE_OPENAI_KEY}",
      "default_chat_model": "gpt-4",
      "default_inline_completion_model": "gpt-4"
    }
  }
}

vLLM, TGI, or any local OpenAI-compatible server:

{
  "providers": {
    "openai-compatible": {
      "base_url": "http://internal-vllm.example.com:8000/v1",
      "api_key": "any-string-the-server-accepts",
      "default_chat_model": "meta-llama/Meta-Llama-3-70B-Instruct"
    }
  }
}

LiteLLM proxy (so you can route to many upstream models from one place, including Bedrock, Vertex, etc.):

{
  "providers": {
    "litellm-compatible": {
      "base_url": "https://litellm.internal.example.com",
      "api_key": "${LITELLM_TOKEN}"
    }
  }
}

Bake the base config into your image and let users select their model from the dropdown.


Custom CA certs and corporate proxies

NBI's HTTP requests use Python's requests and httpx, plus the litellm, openai, and anthropic SDKs. All honor standard Python TLS and proxy environment variables:

REQUESTS_CA_BUNDLE=/etc/pki/tls/certs/ca-bundle.crt
SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt
HTTPS_PROXY=http://corp-proxy.example.com:3128
HTTP_PROXY=http://corp-proxy.example.com:3128
NO_PROXY=localhost,127.0.0.1,.cluster.local

Set these on the pod (c.KubeSpawner.environment for Hub; environment in your Dockerfile or compose file otherwise). The Claude Code CLI is a separate Node.js process and reads the same HTTPS_PROXY and NODE_EXTRA_CA_CERTS conventions.

The frontend talks only to its own Jupyter Server backend, which proxies the LLM calls. The browser does not need a corporate CA trust.


Air-gap deployment

Steps for deploying to a network with no general internet egress:

  1. Pre-build the Docker image with NBI installed, the Claude Code CLI binary baked in, and any MCP server packages pre-installed (do not rely on npx -y at runtime).

  2. Manifest hosting. Set NBI_SKILLS_MANIFEST to either a file:// path (a manifest baked into the image) or an internal https:// URL on a network the pod can reach.

  3. Skill bundles. Either bake the skills into the image under ~/.claude/skills/ (managed status will reset since they aren't from the manifest), or host the GitHub-style tarballs at an internal mirror and write the manifest URLs to point at it.

  4. Disable user-initiated GitHub imports at the network layer — block github.com, codeload.github.com, and raw.githubusercontent.com. Users can still install skills from the local filesystem by dropping bundles into ~/.claude/skills/.

  5. MCP npx -y is incompatible with air-gap. Pre-install the server binary and reference it directly:

    {
      "mcpServers": {
        "filesystem": {
          "command": "/opt/mcp/bin/mcp-server-filesystem",
          "args": ["/home/user/work"]
        }
      }
    }
  6. LLM endpoint. Air-gap requires a self-hosted endpoint (vLLM, TGI, or a LiteLLM proxy in front of a VPC-endpoint Bedrock, etc.). See Self-hosted LLM endpoints.


HIPAA / sensitive-data preset

For deployments that must not transmit PHI to cloud LLM providers, force local-only models:

# jupyter_server_config.py
c.NotebookIntelligence.disabled_providers = [
    "github-copilot",
    "openai-compatible",
    "litellm-compatible",
]
c.NotebookIntelligence.allow_enabling_providers_with_env = False  # users cannot override
c.NotebookIntelligence.disabled_tools = ["nbi-command-execute", "nbi-file-edit"]
c.NotebookIntelligence.allow_enabling_tools_with_env = False

Pair with <env-prefix>/share/jupyter/nbi/config.json shipping an Ollama provider preconfigured against your local model:

{
  "default_provider": "ollama",
  "providers": {
    "ollama": {
      "base_url": "http://ollama-internal.example.com:11434",
      "default_chat_model": "llama3:70b",
      "default_inline_completion_model": "codellama:7b"
    }
  }
}

Block egress to all external LLM hosts at the network layer as defense in depth (see PRIVACY.md for the full list).

This is a starting point, not a HIPAA compliance certification. Run a security review of the full stack (Ollama, your Jupyter image, KubeSpawner, network policy) before treating any data as protected.


Restricting features for managed deployments

NBI's denylist for providers and tools follows the same shape:

Disabling LLM providers

c.NotebookIntelligence.disabled_providers = ["ollama", "litellm-compatible", "openai-compatible"]

Valid IDs: github-copilot, ollama, litellm-compatible, openai-compatible.

To allow per-pod re-enable via env var:

c.NotebookIntelligence.allow_enabling_providers_with_env = True
NBI_ENABLED_PROVIDERS=github-copilot,ollama

Disabling built-in tools

c.NotebookIntelligence.disabled_tools = ["nbi-notebook-execute", "nbi-python-file-edit"]

Valid IDs: nbi-notebook-edit, nbi-notebook-execute, nbi-python-file-edit, nbi-file-edit, nbi-file-read, nbi-command-execute.

To allow per-pod re-enable:

c.NotebookIntelligence.allow_enabling_tools_with_env = True
NBI_ENABLED_BUILTIN_TOOLS=nbi-notebook-execute,nbi-python-file-edit

NBI does not currently support an explicit allowlist mode (allowed_providers, allowed_tools). A new built-in provider added in a minor release would auto-enable for users with disabled_providers=[]. If this matters for your compliance posture, pin to specific NBI versions and review changelog entries before upgrading. Tracked as a feature request.

Disabling coding-agent launcher tiles

The JupyterLab launcher shows a tile for each coding-agent CLI on PATH: Claude Code, opencode, Pi, GitHub Copilot CLI, and Codex. Tile visibility is gated by CLI presence; to hide a tile even when the CLI is present (for example, to keep users in the chat sidebar's audit path), add its ID to the denylist:

c.NotebookIntelligence.disabled_coding_agent_launchers = ["opencode", "pi", "codex"]

Valid IDs: claude-code, opencode, pi, github-copilot-cli, codex. Unknown IDs raise at server startup so a typo can't silently no-op the policy.

The github-copilot-cli ID is deliberately distinct from github-copilot (the disabled_providers value for the Copilot LLM provider). The tile and the provider are independent surfaces; hiding the tile does not affect chat with the Copilot provider, and vice versa.

To vary the policy per spawn profile, opt into per-pod re-enable:

c.NotebookIntelligence.allow_enabling_coding_agent_launchers_with_env = True
NBI_ENABLED_CODING_AGENT_LAUNCHERS=claude-code,codex

The env var has effect only when allow_enabling_coding_agent_launchers_with_env = True; without that flag, the denylist is final. The merged effective set is computed per request and the frontend re-evaluates tile visibility on each capabilities refresh, so an env-var flip applies after a page reload in the same session. Edits to the disabled_coding_agent_launchers traitlet in jupyter_server_config.py require a JupyterLab server restart, the same as other traitlet edits.

Blast radius. The denylist hides the launcher tile and removes the matching JupyterLab command-palette entry. The CLI binary remains on PATH and remains usable from a manually-opened terminal. To prevent terminal use, restrict the binary at the container-image or PATH level. To prevent the Claude chat mode itself (a separate surface from the launcher tile), use NBI_CLAUDE_MODE_POLICY=force-off from the Admin policies table. Note that NBI_CLAUDE_MODE_POLICY=force-off does not imply hiding the Claude Code launcher tile: the tile runs claude directly in a terminal and is independent of the chat-mode SDK backend. To hide both surfaces, combine NBI_CLAUDE_MODE_POLICY=force-off with disabled_coding_agent_launchers = ["claude-code"].

Per-pod re-enable trust model. Setting allow_enabling_coding_agent_launchers_with_env = True delegates the final denylist decision to whatever process sets NBI_ENABLED_CODING_AGENT_LAUNCHERS on the pod. If your spawn-profile config is itself user-influenced (profile-form fields, untrusted YAML), keep this flag False so the traitlet baseline is authoritative.

Disabling user-initiated GitHub Skill imports

c.NotebookIntelligence.allow_github_skill_import = False

Hides the Import from GitHub button in the Skills panel and rejects POSTs to /notebook-intelligence/skills/import and /notebook-intelligence/skills/import/preview with HTTP 403. This does not disable the managed-skills reconciler; admin-curated skills delivered via NBI_SKILLS_MANIFEST continue to install. Use this when you want to allow only org-vetted skills.

To vary the policy per spawn profile, override at pod startup:

NBI_ALLOW_GITHUB_SKILL_IMPORT=false

The env var wins over the traitlet and is resolved at server startup. Recognized values: true/false/1/0/yes/no/on/off, case-insensitive. Unrecognized values raise at startup so a typo can't silently flip the policy.

Tuning the chat-sidebar workspace file picker

Audience: server admins (and end users who edit ~/.jupyter/nbi/config.json directly). The setting is not exposed in the Settings dialog.

The @-mention picker in the chat sidebar enumerates files from the JupyterLab working directory and skips a built-in set of directories (__pycache__, node_modules) plus any dotfiles/dot-directories. Because dot-prefixed names are filtered separately, entries starting with . are no-ops; list non-dot names only.

Match is by directory name only (not path), case-sensitive. Use this when a project has standard build outputs the picker shouldn't surface.

Four layers can contribute, all additive (each layer can only add new skipped names, never re-expose names skipped by an earlier layer):

  1. Traitlet in jupyter_server_config.py:

    c.NotebookIntelligence.additional_skipped_workspace_directories = ["build", "dist", "target"]
  2. Env var at pod startup (per spawn profile):

    NBI_ADDITIONAL_SKIPPED_WORKSPACE_DIRECTORIES=tmp,artifacts
  3. Env-prefix NBI config at <env-prefix>/share/jupyter/nbi/config.json (org-wide, baked into the image — useful when the deployment doesn't manage jupyter_server_config.py but does ship a curated config file):

    { "additional_skipped_workspace_directories": ["coverage", "out"] }
  4. User NBI config at ~/.jupyter/nbi/config.json (per-user extension on top of the admin baseline):

    { "additional_skipped_workspace_directories": [".terraform"] }

Duplicates are collapsed; the merged list is then added to the built-in skip set on the frontend. Edits to config.json require a JupyterLab restart to take effect, matching the rest of NBI config — there's no UI control (issue #232).

Disabling the Skills tab

c.NotebookIntelligence.skills_management_policy = "force-off"

Or via env: NBI_SKILLS_MANAGEMENT_POLICY=force-off.

Force-off does three things at once:

  • Hides the Skills tab in the Settings panel.
  • Returns HTTP 403 from every /notebook-intelligence/skills/* route, so a stale frontend or a direct API caller can't read or write skills.
  • Suppresses the managed-skills reconciler — the manifest is treated as empty, no SkillReconciler is constructed, and no scheduled reconcile runs. Org-curated skills still on disk are not touched, but new manifests aren't pulled. Takes effect on JupyterLab server restart. For incident-response without a restart, see the kill switch below.

Stopping a running reconciler without a server restart

If the manifest URL or the managed-skills token is compromised and you need to stop the in-flight reconciler immediately, two non-restart paths are now available:

  1. HTTP kill switch: POST /notebook-intelligence/skills/reconciler/stop. Authenticated (Jupyter session token). Stops the background reconciler and returns {"stopped": true, "was_running": <bool>}. Idempotent; safe to script across pods.

    curl -X POST -H "Authorization: token $JUPYTER_TOKEN" \
         https://hub.example.com/user/<name>/notebook-intelligence/skills/reconciler/stop
  2. Env-var flip: the reconciler re-reads NBI_SKILLS_MANAGEMENT_POLICY at the start of each cycle and self-stops if it reads force-off. If your platform supports in-place pod env updates (rare), this fires the kill switch on the next reconcile boundary (default 24h). For most deployments the HTTP route is the faster option.

Either mechanism only stops the background thread; existing skill bundles on disk remain. Use claude or filesystem tooling to remove them if needed.

No live restart. Once stopped, the reconciler stays stopped for the life of the JupyterLab process. There is no /start companion endpoint; bouncing the server is the only path to re-enable reconciliation in the same pod. This is intentional: a kill switch that another script can flip back on isn't a kill switch.

Per-user trust. The endpoint is authenticated with the user's Jupyter session token, not an admin claim. In hub deployments where the JupyterLab pod owner is not the policy admin (typical for JupyterHub), a tenant can stop their own pod's reconciler. The reconciler is per-pod, so this only affects that user's managed-skills delivery. For deployments that need to prevent self-stop, leave skills_management_policy at user-choice and rely on the manifest-fetch token's scope as the auth boundary instead.

Use this section's full-disable when an org wants to disable user-authored Claude skills entirely.

Blast radius. Force-off only kills the management UI — skill bundles already on disk under ~/.claude/skills/ or a project's .claude/skills/ keep being discovered by Claude Code itself because Claude's skill loader doesn't consult NBI's policy. To stop existing skills from loading, remove them on disk before flipping the policy.

Disabling the Claude-mode MCP Servers tab

c.NotebookIntelligence.claude_mcp_management_policy = "force-off"

Or via env: NBI_CLAUDE_MCP_MANAGEMENT_POLICY=force-off.

Force-off:

  • Hides the Claude-mode MCP Servers tab in the Settings panel (visible only when Claude mode is on and the claude CLI is available).
  • Returns HTTP 403 from every /notebook-intelligence/claude-mcp/* route.

The Claude-mode tab is independent of the existing non-Claude MCP Servers tab. The former wraps Claude Code's own config (~/.claude.json and project .mcp.json); the latter manages NBI's own MCP servers used by the non-Claude chat path. They never appear at the same time — the non-Claude tab is hidden when Claude mode is on, and the Claude-mode tab is hidden when it's off.

Reads come from Claude's JSON config files directly (fast, no health checks). Writes (add / remove) shell out to claude mcp add / claude mcp remove so Claude remains the source of truth for any side effects (project-trust prompts, OAuth bookkeeping).

Blast radius. Force-off only kills the management UI — MCP servers already configured in ~/.claude.json or <cwd>/.mcp.json keep loading inside Claude Code sessions because Claude's MCP loader doesn't consult NBI's policy. To stop existing servers, remove them on disk (or via the claude mcp remove CLI) before flipping the policy.

Trust model. MCP servers run as subprocesses (stdio transport) or accept arbitrary URLs (sse/http transport) inside Claude Code sessions; NBI does not validate or sandbox the command, environment, or network endpoint beyond rejecting CLI flag-smuggling. For multi-tenant or regulated deployments, default to claude_mcp_management_policy = force-off and ship a curated set of servers via ~/.claude/settings.json instead.

Disabling the Plugins tab

c.NotebookIntelligence.claude_plugins_management_policy = "force-off"

Or via env: NBI_CLAUDE_PLUGINS_MANAGEMENT_POLICY=force-off.

Force-off hides the Plugins tab and returns 403 from every /notebook-intelligence/plugins/* route. The tab is otherwise visible only when Claude mode is on and the claude CLI is available. Both reads (claude plugin list --json) and writes (claude plugin install / uninstall / enable / disable / marketplace add / marketplace remove) shell out to the Claude CLI; Claude owns the plugin state under ~/.claude/plugins/.

Blast radius. Force-off only kills the management UI — already-installed plugins keep loading inside Claude Code sessions because Claude's plugin loader doesn't consult NBI's policy. To stop existing plugins from loading, you'd need to remove them on disk or disable them via the claude plugin disable CLI before flipping the policy. Force-off prevents user-driven add/remove/enable/disable through NBI; that's the contract.

To allow user-driven plugin management but block GitHub-sourced marketplaces:

c.NotebookIntelligence.allow_github_plugin_import = False

Or via env: NBI_ALLOW_GITHUB_PLUGIN_IMPORT=false (also accepts true/1/0/yes/no/on/off). When False, the "From GitHub" affordance in the Plugins panel hides itself and the backend rejects marketplace-add requests whose source is a GitHub URL, owner/repo shorthand, or git@github.com: reference. Local-path and arbitrary-URL sources remain available. This is finer-grained than claude_plugins_management_policy = force-off, which kills the entire surface.

Trust model. Plugins installed via claude plugin install execute as part of Claude Code sessions; NBI does not signature-verify or sandbox them, and the claude CLI's validation is best-effort. The marketplace-add path is a network fetch (server-side) — for multi-tenant or regulated deployments, default to claude_plugins_management_policy = force-off and curate plugins server-side, or restrict marketplaces to vetted sources only.

GitHub auth for marketplace add

When the marketplace source is a GitHub URL or owner/repo shorthand, NBI resolves a token with the same precedence as Skills' GitHub import:

  1. GITHUB_TOKEN env var (server-process scope)
  2. GH_TOKEN env var
  3. gh auth token subprocess output (only if gh is on PATH)

Resolved tokens are injected into the claude plugin marketplace add subprocess via env, never argv — they do not appear in DEBUG logs. The chain is re-evaluated per call, so rotating the token (env update or gh auth refresh) takes effect on the next add. Required scope: repo for classic PATs, or contents:read on the target repo for fine-grained PATs. When gh is not installed the third step short-circuits silently; rely on GITHUB_TOKEN instead.

GitHub Enterprise: the detector recognizes GHE hosts that an admin declares via NBI_GITHUB_ENTERPRISE_HOSTS (CSV). Without that env, only public github.com is recognized, and a GHE marketplace URL falls through to anonymous git auth AND silently bypasses allow_github_plugin_import = False. Declare every host that should be treated as GitHub:

# Exact host matches only — safest default.
NBI_GITHUB_ENTERPRISE_HOSTS=github.acme.com,ghe.example.com

Tokens follow cookie-domain semantics: a bare token matches the exact host only; a leading-dot token (.acme.com) matches every subdomain of acme.com. Subdomain matching is opt-in because if suffix-matching were the default, declaring acme.com would silently inject GITHUB_TOKEN into any *.acme.com corp service (jira, artifactory, etc.) that someone happened to point marketplace-add at. Prefer the exact form; reach for the leading-dot form only when you actually have multiple GitHub subdomains under one apex.

# Explicit opt-in to subdomain matching: covers github.acme.com,
# ghe.acme.com, and any future *.acme.com GitHub-flavored host.
NBI_GITHUB_ENTERPRISE_HOSTS=.acme.com

The matcher rejects lookalikes that aren't actual subdomains, so github.acme.com.evil.test is correctly excluded regardless of the token shape.

This env is independent of NBI_GHE_SUBDOMAIN (which only configures GitHub Copilot's OAuth tenant). The two settings serve different surfaces; set whichever applies to your deployment.

For air-gap deployments, marketplace-add inherits the JupyterLab process env, so the same HTTPS_PROXY / HTTP_PROXY / NO_PROXY / NODE_EXTRA_CA_CERTS settings documented in Custom CA certs and corporate proxies apply. Pre-installed plugins (under ~/.claude/plugins/) keep loading without any network access.

Disabling terminal drag-drop file attach

c.NotebookIntelligence.terminal_drag_drop_policy = "force-off"

Or via env: NBI_TERMINAL_DRAG_DROP_POLICY=force-off.

Force-off hides the per-terminal drag-drop toolbar toggle and rejects upload-staging POSTs from a terminal context. Drag-drop is enabled by default; flip it off in regulated tenants where the staging file write or the resulting @-mention path is undesirable.

Disabling the open-files refresh watcher

c.NotebookIntelligence.refresh_open_files_on_disk_change_policy = "force-off"

Or via env: NBI_REFRESH_OPEN_FILES_ON_DISK_CHANGE_POLICY=force-off.

The refresh watcher reloads open notebook and file editor tabs when their content changes on disk (for example, when an agent edits a file). It skips tabs with unsaved local edits. Enabled by default; users can opt out in the NBI Settings dialog. Use force-off in deployments where automatic reloads could surprise users editing the same files via external tooling, or force-on to mandate the behavior tenant-wide.


Terminal drag-drop and chat-sidebar file attach both write to the shared upload-staging directory under the JupyterLab process's tempfile.gettempdir(). Two tunables govern that endpoint:

Env var Default Behavior
NBI_UPLOAD_MAX_MB 50 Per-file size cap (megabytes). Over the cap returns HTTP 413. 0 disables.
NBI_UPLOAD_RETENTION_HOURS 24 How long staged uploads survive before the next upload sweeps them. 0 keeps only the atexit purge (files survive the session).

Trust model. Staged files live in the user's tempfile.gettempdir() and inherit the directory's POSIX permissions. The same nbi-file-read denylist that scopes general file reads does not apply to upload-staged files because they sit outside the Jupyter root. For sensitive-data tenants, set NBI_UPLOAD_RETENTION_HOURS=0 to skip retention beyond the session and pair with terminal_drag_drop_policy = force-off to keep the surface to chat-sidebar attachments only.


Multi-tenancy and per-team scoping

JupyterHub spawn-time profiles can carry per-team config:

c.KubeSpawner.profile_list = [
    {
        "display_name": "Data Science (managed skills)",
        "kubespawner_override": {
            "environment": {
                "NBI_SKILLS_MANIFEST": "https://manifests.internal/team-ds.yaml",
                "NBI_MANAGED_SKILLS_TOKEN": {"valueFrom": {"secretKeyRef": {...}}},
            },
        },
    },
    {
        "display_name": "ML Research (no managed skills)",
        "kubespawner_override": {"environment": {"NBI_SKILLS_MANIFEST": ""}},
    },
]

The reconciler handles skill name collisions between manifests and user-authored skills per entry (managed entries skip user-authored skills with the same name). Across teams, if a user moves between profiles that ship different data-eda skills, they see whichever team's skill the active profile reconciled most recently. Keep team skill names distinct (team-ds-data-eda, team-ml-data-eda) to avoid surprises.


Managed Claude Skills token

NBI_MANAGED_SKILLS_TOKEN (or the managed_skills_token traitlet) authenticates managed-skills GitHub operations (manifest fetch when hosted on github.com, commits-API probing, tarball downloads).

  • Minimum scope: contents:read on the org/repos that host the manifest and skill bundles.
  • Rotation: NBI reads the token from env on every reconcile cycle. Restart the pod (or reissue the env, if your KubeSpawner re-reads on resume) to rotate.
  • 401/403 behavior: a single auth failure on a managed operation is logged and not retried with the fallback token chain when NBI_MANAGED_SKILLS_TOKEN is set. This keeps misconfiguration visible. The reconciler continues with remaining entries.
  • User-initiated imports do not see this token. They use GITHUB_TOKENGH_TOKENgh CLI auth. Keep these separate so a misconfigured org token can't unintentionally apply to user imports.

Chat feedback event hook

When enable_chat_feedback = True, NBI emits a telemetry event in-process whenever a user gives thumbs-up/down feedback in chat. The event payload includes the rating, the prompt, the response, and the model.

The event is emitted in-process only. Nothing leaves the process unless you write a custom handler that listens for it. The payload shape is not currently considered stable API; if you build on it, pin to a specific NBI version.

To pipe feedback into your internal observability stack (Kafka, OTel collector), write an extension that registers a listener for the telemetry event and forwards it.


HTTP API surface

All routes live under /notebook-intelligence/. All require Jupyter authentication (XSRF token plus Jupyter login token) including the /copilot WebSocket upgrade, which now inherits Jupyter's WebSocketMixin + JupyterHandler so the same allow_origin and identity-provider checks that apply to REST handlers also apply to the chat WS endpoint. The labextension obtains these automatically. There is no admin-only route; access control runs through Jupyter Server itself.

Route Method Purpose
/notebook-intelligence/capabilities GET Capabilities + tool/provider gate state.
/notebook-intelligence/config GET/POST Read or update user-scope config.
/notebook-intelligence/update-provider-models POST Refresh model list for a provider (e.g., Anthropic SDK refresh).
/notebook-intelligence/mcp-config-file GET/POST Read or write ~/.jupyter/nbi/mcp.json.
/notebook-intelligence/reload-mcp-servers POST Re-discover MCP servers without restarting JupyterLab.
/notebook-intelligence/emit-telemetry-event POST Used by the frontend to emit telemetry events (e.g., chat feedback).
/notebook-intelligence/gh-login-status GET GitHub Copilot login state.
/notebook-intelligence/gh-login POST Begin GitHub Copilot device-flow login.
/notebook-intelligence/gh-logout GET Sign out of GitHub Copilot.
/notebook-intelligence/copilot WS Streaming chat / inline-completion WebSocket.
/notebook-intelligence/rules GET List discovered rules.
/notebook-intelligence/rules/<id>/toggle PUT Toggle a rule's active field.
/notebook-intelligence/rules/reload POST Manually reload all rules.
/notebook-intelligence/skills GET/POST List or create skills.
/notebook-intelligence/skills/context GET Skill context info for the active workspace.
/notebook-intelligence/skills/import/preview POST Preview a GitHub-hosted skill before installing.
/notebook-intelligence/skills/import POST Install a GitHub-hosted skill (user-initiated).
/notebook-intelligence/skills/reconcile POST Run the managed-skills reconciler. Returns 409 if NBI_SKILLS_MANIFEST is unset.
/notebook-intelligence/skills/reconciler/stop POST Incident-response kill switch. Stops the background reconciler without a server restart. Idempotent.
/notebook-intelligence/skills/<scope>/<name> GET/PUT/DELETE Skill detail; managed skills are read-only.
/notebook-intelligence/skills/<scope>/<name>/rename POST Rename a skill (denied for managed skills).
/notebook-intelligence/skills/<scope>/<name>/files GET/POST/DELETE Skill bundle file ops.
/notebook-intelligence/skills/<scope>/<name>/files/rename POST Rename a file inside a skill bundle.
/notebook-intelligence/upload-file POST Upload a file to attach as chat context (size and retention governed by upload_max_mb / upload_retention_hours).
/notebook-intelligence/claude-sessions GET List Claude Code sessions for the working directory.
/notebook-intelligence/claude-sessions/resume POST Resume a Claude session.
/notebook-intelligence/claude-mcp GET/POST List or add Claude-mode MCP servers. Gated by claude_mcp_management_policy.
/notebook-intelligence/claude-mcp/<scope>/<name> GET/DELETE Get or remove a Claude-mode MCP server by scope (user/project/local) and name.
/notebook-intelligence/plugins GET/POST List or install Claude plugins. Gated by claude_plugins_management_policy.
/notebook-intelligence/plugins/<scope>/<name> POST/DELETE Enable or disable (POST with {"action": "enable"|"disable"}) or uninstall (DELETE) a plugin.
/notebook-intelligence/plugins/marketplace GET/POST List or add plugin marketplaces. GitHub-sourced adds are gated by allow_github_plugin_import.
/notebook-intelligence/plugins/marketplace/<name> DELETE Remove a plugin marketplace.

The extension respects c.ServerApp.base_url. Behind JupyterHub at /user/<name>/ everything still works because JupyterLab proxies routes through the per-user base URL automatically.


Failure modes

Condition User-visible behavior Where to look in logs
LLM provider unreachable Chat shows "Thinking…", then a connection-error toast. JupyterLab terminal (server stderr).
LLM 401 (bad or expired key) Chat shows the provider's error message. JupyterLab terminal; the provider's own SDK logs.
Claude CLI missing Chat hangs on "Thinking…" in Claude mode; never returns. JupyterLab terminal — claude-agent-sdk connect failure.
Claude CLI fails to start (path mismatch) Same as above. Same — set NBI_CLAUDE_CLI_PATH and restart JupyterLab.
MCP stdio server crashes The server's tools disappear from @mcp chat participant. JupyterLab terminal — server's stderr.
MCP npx -y package fetch fails (offline) Server fails to start; tools missing. JupyterLab terminal.
Managed-skills manifest 5xx / DNS failure Reconciler logs the error; existing managed skills remain installed. JupyterLab terminal — skill_reconciler warning/error.
Managed-skills tarball fetch fails (per entry) That entry stays at the previously installed version; others succeed. JupyterLab terminal — per-entry error.
NBI_MANAGED_SKILLS_TOKEN 401/403 Reconcile fails; loud log; does not fall back to the GITHUB_TOKEN chain. JupyterLab terminal.
Ruleset frontmatter is invalid YAML Rule is skipped; others load. JupyterLab terminal — rule_manager warning.
Encrypted token decrypt fails The chat sidebar prompts the user to sign in again. JupyterLab terminal.
claude plugin install fails (network, auth) Plugin row stays unchanged; install button surfaces the CLI's stderr. JupyterLab terminal (plugin_manager warning).
claude plugin marketplace add from GHE Without NBI_GITHUB_ENTERPRISE_HOSTS, falls through to anonymous git auth and may fail without a clear message. JupyterLab terminal; see GHE caveat in GitHub auth for marketplace add.
Copilot model-list endpoint fails Chat-model dropdown silently falls back to the hardcoded list. JupyterLab terminal (github_copilot warning).
Upload exceeds NBI_UPLOAD_MAX_MB Terminal drag-drop and chat-sidebar attach both return HTTP 413. JupyterLab terminal; check upload_max_mb traitlet.

Version matrix

NBI is tested against the JupyterLab and jupyter_server versions declared in pyproject.toml.

NBI version JupyterLab jupyter_server Python
5.0.x 4.x 2.x 3.10+
4.8.x 4.x 2.x 3.10+
4.7.x 4.x 2.x 3.10+
4.6.x 4.x 2.x 3.10–3.12
4.5.x 4.x 2.x 3.10–3.12
4.4.x 4.x 2.x 3.10–3.12
4.3.x 4.x 2.x 3.10–3.12

Upper bounds for litellm, claude-agent-sdk, anthropic, and mcp are not pinned in pyproject.toml. For production deployments, pin these in your image build:

pip install \
  "notebook-intelligence==5.0.*" \
  "litellm==1.83.*" \
  "claude-agent-sdk==0.x.*" \
  "anthropic==0.x.*" \
  "mcp==1.27.*"

Substitute the versions you've validated. As of 5.0.0 NBI uses the official mcp Python SDK (the prior fastmcp dependency was removed); see the 5.0.0 changelog migration note if your image previously pinned fastmcp. The NBI test suite is currently TypeScript-only (jlpm test); end-to-end Python testing is a future work item.


FIPS posture

NBI's cryptography dependency is used solely to encrypt the stored GitHub Copilot token. The default password (nbi-access-token-password) and the encryption are intended for at-rest obfuscation, not as a FIPS-validated secret store.

If you operate under FIPS:

  • Run with the FIPS-mode OpenSSL build of Python. NBI's cryptography calls (Fernet under the hood — AES-128-CBC plus HMAC-SHA256) work under FIPS-mode OpenSSL.
  • Set a per-user NBI_GH_ACCESS_TOKEN_PASSWORD so the encryption key is not derivable.
  • For higher assurance, disable "remember GitHub Copilot login" entirely so no encrypted-at-rest token exists.

NBI itself does not assert FIPS compliance.


Resource footprint

NBI's per-user memory cost depends on which Python dependencies actually get imported. A clean Jupyter Server with NBI installed but no LLM activity uses roughly the baseline of notebook_intelligence plus its server-imported dependencies.

We have not published measured numbers. If you size pods aggressively, profile your image: import everything NBI imports lazily (run a chat turn, trigger inline completion, exercise claude-agent-sdk) and measure RSS before sizing your pod memory request. Inline completion under load is the chattiest path; consider provider-side rate limits if you have hundreds of simultaneous users on a paid endpoint.

A measured-baseline document is on the roadmap. If you have numbers from a production deployment, share them in a GitHub issue.