Skip to content

feat(launcher): admin policy to hide coding-agent launcher tiles#288

Merged
mbektas merged 2 commits into
plmbr:mainfrom
pjdoland:feat/coding-agent-launcher-policy
May 18, 2026
Merged

feat(launcher): admin policy to hide coding-agent launcher tiles#288
mbektas merged 2 commits into
plmbr:mainfrom
pjdoland:feat/coding-agent-launcher-policy

Conversation

@pjdoland
Copy link
Copy Markdown
Collaborator

Summary

Adds an admin denylist that hides JupyterLab launcher tiles for coding-agent CLIs (Claude Code, opencode, Pi, GitHub Copilot CLI, Codex) even when the corresponding binary is on `PATH`. Mirrors the existing `disabled_providers` / `allow_enabling_providers_with_env` / `NBI_ENABLED_*` shape so the mental model carries from one denylist to the next.

Closes the audit gap from the post-4.8.0 features review: the five coding-agent launcher tiles were the only new user-facing surface without an admin gate. An enterprise admin can now:

  • Allow Claude chat mode in the sidebar but hide the Claude Code launcher tile (to keep users on the chat-sidebar audit path).
  • Block all terminal-based coding agents because they bypass corporate egress proxies.
  • Allow only specific tiles per spawn profile via the per-pod re-enable env.

The shape

  • `disabled_coding_agent_launchers` (List traitlet, default `[]`). Valid IDs: `claude-code`, `opencode`, `pi`, `github-copilot-cli`, `codex`.
  • `allow_enabling_coding_agent_launchers_with_env` (Bool traitlet, default `False`).
  • `NBI_ENABLED_CODING_AGENT_LAUNCHERS` (CSV env). Only effective when `allow_enabling_*=True`.

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 one does not affect the other.

Security posture

A six-agent review surfaced three security-relevant items, all addressed:

  1. Command-palette bypass. The custom Claude Code command's `isVisible` only checked CLI availability. The four `registerAgentCliLauncher`-based commands already checked the policy, but the Claude Code one didn't. Fixed so policy gates both the launcher tile and the palette entry.
  2. Frontend fail-open. The previous `isCodingAgentLauncherDisabledByPolicy` returned `false` when capabilities hadn't loaded or the field was malformed. The denylist would silently disappear if a future change ever pre-seeded `isClaudeCliAvailable`. Now fails closed: a missing or non-array field hides the tile.
  3. Startup validation. Unknown IDs raise `ValueError` during NBI extension load (NBI fails to register on the pod; the rest of JupyterLab keeps running). Loud-fail-on-typo matches `_resolve_bool_with_env`'s convention.

Testing

  • `pytest tests/` (736 passed; 15 new in `tests/test_coding_agent_launchers.py` covering valid-ID set, env CSV parsing including a flipped substring-vs-token test that actually distinguishes the two, effective-set merge across all four input combinations, and the startup validator).
  • `jlpm jest` (160 passed; 6 new in `tests/ts/coding-agent-launchers.test.ts` for the fail-closed semantics and the deliberate `github-copilot` vs `github-copilot-cli` distinction).
  • `jlpm tsc --noEmit`, `jlpm lint:check` clean.
  • End-to-end smoke: imported `NotebookIntelligence`, set the new traitlets, confirmed defaults and value round-trips.

After the review, the test helpers `compute_effective_disabled_launchers` and `validate_coding_agent_launcher_ids` were extracted from the handler / setup paths into `util.py`, so the tests now pin the real production logic instead of an inline re-implementation.

Risks / follow-ups

  • The denylist hides UI only. The CLI binary remains on `PATH` and a user can still type `claude` / `opencode` / etc. in a manually-opened terminal. Restrict at the container-image or `PATH` level to prevent that. Spelled out in the admin guide's blast-radius callout.
  • `NBI_CLAUDE_MODE_POLICY=force-off` does NOT imply hiding the Claude Code launcher tile. The two surfaces are independent (the tile runs `claude` in a terminal; the policy gates the chat-sidebar SDK backend). Documented; consider coupling in a future PR if the maintainers want least-surprise behavior.
  • Unrelated bug noticed but not fixed here: `is_provider_enabled_in_env` doesn't strip whitespace, so `NBI_ENABLED_PROVIDERS="github-copilot, ollama"` currently fails to re-enable `ollama`. Worth a follow-up PR.

Picked up out of the post-4.8.0 feature audit; closes that gap for the coding-agent launcher surface specifically.

Adds a denylist that lets admins hide JupyterLab launcher tiles for
coding-agent CLIs (Claude Code, opencode, Pi, GitHub Copilot CLI, Codex)
even when the corresponding binary is on PATH. Mirrors the
disabled_providers / allow_enabling_providers_with_env / NBI_ENABLED_*
shape so the mental model carries.

The shape:

- disabled_coding_agent_launchers (List traitlet, valid IDs
  claude-code, opencode, pi, github-copilot-cli, codex)
- allow_enabling_coding_agent_launchers_with_env (Bool traitlet)
- NBI_ENABLED_CODING_AGENT_LAUNCHERS (per-pod re-enable CSV env)

The github-copilot-cli ID is deliberately distinct from github-copilot
(the disabled_providers value for the Copilot LLM provider) so an admin
can disable one surface without touching the other.

Startup validates unknown IDs and fails the extension load loudly per
the same loud-fail convention as _resolve_bool_with_env. Frontend gate
fails closed: a missing or malformed capabilities field hides the tile
rather than silently bypassing the denylist. The Claude Code command
palette entry is gated on the same policy to keep parity with the
register_agent_cli_launcher path.

Six-agent review applied:
- Extracted compute_effective_disabled_launchers and
  validate_coding_agent_launcher_ids to util.py so tests pin the real
  production logic (test-architect).
- Fixed Claude Code command-palette bypass (security; the bespoke
  command's isVisible only checked CLI availability, unlike the four
  CLIs that go through registerAgentCliLauncher).
- Made the frontend gate fail closed on missing / malformed capabilities
  (security; the previous "fail open" relied on isClaudeCliAvailable
  also defaulting false, which is a fragile defense).
- Flipped the env-var substring test to actually distinguish token-match
  from substring-match.
- Tightened admin-guide hot-reload sentence to reflect that traitlet
  edits need a server restart.
- Documented NBI_CLAUDE_MODE_POLICY interaction and the per-pod
  re-enable trust model in the admin guide.

Skipped: the duplicated NBI_ENABLED_PROVIDERS whitespace-handling bug
in the older is_provider_enabled_in_env helper. Real bug but unrelated
to this PR's surface; worth a follow-up.
@pjdoland pjdoland added the enhancement New feature or request label May 17, 2026
@mbektas
Copy link
Copy Markdown
Collaborator

mbektas commented May 18, 2026

@pjdoland can you resolve the conflicts?

…auncher-policy

# Conflicts:
#	docs/admin-guide.md
@pjdoland
Copy link
Copy Markdown
Collaborator Author

Merge conflicts are resolved.

@mbektas mbektas merged commit 511d988 into plmbr:main May 18, 2026
3 checks passed
pjdoland added a commit to pjdoland/notebook-intelligence that referenced this pull request May 22, 2026
Promotes the [Unreleased] CHANGELOG snapshot to [5.0.0] - 2026-05-22
and expands it to cover everything merged into upstream/main after
PR plmbr#287's docs refresh. Bumps package.json to 5.0.0.

CHANGELOG additions cover the post-plmbr#287 surface:

- Settings tabs: plugin marketplace picker (plmbr#284), plugin marketplace
  details + Update button (plmbr#303), per-workspace MCP disable (plmbr#286),
  JSON-paste path in Add MCP server (plmbr#285).
- Launchers: hide-with-policy (plmbr#288), brand icons for Codex / opencode
  (plmbr#325, plmbr#333), per-launch directory picker (plmbr#332).
- Chat sidebar and agentic UX: workspace @-mention in Claude mode
  (plmbr#327), reload-open-files-on-disk (plmbr#330), steered system prompt
  away from over-eager notebook creation (plmbr#336).
- Skills: multi-manifest support (plmbr#321), tracks-upstream for user-
  imported skills (plmbr#322), HTTP kill switch for the reconciler (plmbr#291).
- Accessibility: full sub-section covering plmbr#305-plmbr#320.
- Security: shell-tool sandbox (plmbr#290), Claude UI-bridge sandbox (plmbr#323),
  0o600 on encrypted token (plmbr#293), env-secret scrubbing (plmbr#295), MCP
  config shape validation (plmbr#299), XSS allowlist (plmbr#296), Copilot WS
  auth + origin (plmbr#301), GHE host detection (plmbr#292), fastmcp -> mcp SDK
  swap (plmbr#324).
- Fixed: session listing unification (plmbr#310), session preview unwrap
  (plmbr#331), down-area runtime throw (plmbr#330 follow-up), WS message-handler
  leak (plmbr#294).
- Removed: fastmcp dependency, history.jsonl session gate.

Adds a Migration note covering the five behavior changes operators
should review before upgrading from 4.x: fastmcp swap, path
sandboxes, history.jsonl gate removal, workspace @-mention pointer
shape, and the Copilot WebSocket auth/origin tightening.

Two reviewer rounds (six personas each) applied:
- Round 1 caught security overclaims (plmbr#293, plmbr#299, plmbr#323), the
  plmbr#284/plmbr#303 mis-attribution, missing migration note, 3 em dashes,
  and the stale `fastmcp==2.x.*` recommendation in the admin guide.
- Round 2 caught the missing plmbr#301 migration bullet, missing version-
  matrix 5.0.x row, missing README TOC entry, and a couple of style
  nits (sub-heading overpromise, orphan bullet).

Skipped (deferred to future PRs):
- README first-run tour mention.
- Admin guide HTTP kill-switch row in Failure-modes table.
- Terminal drag-drop trust-model precision update after plmbr#327.
- Cipher description nit in plmbr#293 (Fernet AES-128-CBC+HMAC, not
  AES-GCM).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants