Files
windmill/debugger
Ruben Fiszel 74c418570b fix: forward TLS trust roots to debug sessions and honor INIT_SCRIPT on windmill_extra (#10532)
* fix: forward proxy and TLS settings to debugger subprocesses

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: reach uv and the bun debugger with the forwarded network settings

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: map every CA variable spelling onto the one uv reads

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep package-index credentials out of debugged user code

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: install debugger dependencies outside the interpreter running user code

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: sandbox and bound the debugger dependency installer

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: correct the installer timeout rationale

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: scope the uv --cert note to the commands prepare-deps runs

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: build the debug venv against the interpreter that runs the script

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: do not start the debuggee for a session that already went away

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: remove the debug script when the session is gone before it starts

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 16:25:52 +00:00
..

Windmill Debug Module

A DAP (Debug Adapter Protocol) implementation for debugging Python and TypeScript/Bun scripts in Windmill's Monaco editor.

Overview

This module provides step-through debugging capabilities with breakpoints, variable inspection, and stack traces. It uses WebSocket communication between the Monaco editor frontend and language-specific debug backends.

Supported Languages

  • Python - Uses a bdb-based debugger via dap_websocket_server.py
  • TypeScript/Bun - Uses V8 Inspector Protocol via dap_websocket_server_bun.ts

Architecture

┌─────────────────────┐     WebSocket      ┌──────────────────────────┐
│  Monaco Editor      │◄──────────────────►│  DAP Debug Service       │
│  (dapClient.ts)     │    DAP Protocol    │  (dap_debug_service.ts)  │
└─────────────────────┘                    └──────────┬───────────────┘
                                                      │
                                           ┌──────────┴───────────┐
                                           │                      │
                                    ┌──────▼──────┐       ┌───────▼───────┐
                                    │   Python    │       │   Bun/TS      │
                                    │   Debugger  │       │   Debugger    │
                                    └─────────────┘       └───────────────┘

Files

File Description
dap_debug_service.ts Unified WebSocket server that routes to Python or Bun debuggers
dap_websocket_server.py Python debugger backend (bdb-based)
dap_websocket_server_bun.ts Bun/TypeScript debugger backend (V8 Inspector)
dapClient.ts Client-side DAP WebSocket client with Svelte store
MonacoDebugger.svelte Monaco editor integration component
DebugToolbar.svelte Debug control buttons (step, continue, etc.)
DebugPanel.svelte Variables and stack trace display panel
index.ts Module exports

Usage

Starting the Debug Service

bun run debug/dap_debug_service.ts

Options:

  • --port PORT - Server port (default: 3003)
  • --host HOST - Server host (default: 0.0.0.0)
  • --python-path PATH - Python binary path (default: python3)
  • --bun-path PATH - Bun binary path (default: bun)
  • --nsjail - Enable nsjail sandboxing for debugger processes
  • --nsjail-config PATH - Path to nsjail config file
  • --nsjail-path PATH - Path to nsjail binary (default: nsjail)

Endpoints

  • /python - Python debugging
  • /typescript - TypeScript/Bun debugging
  • /bun - Alias for /typescript

Environment Variables

Variable Description Default
DAP_PORT Server port 3003
DAP_HOST Server host 0.0.0.0
DAP_PYTHON_PATH Python binary path python3
DAP_BUN_PATH Bun binary path bun
DAP_NSJAIL_ENABLED Enable nsjail sandboxing false
DAP_NSJAIL_PATH nsjail binary path nsjail
DAP_NSJAIL_CONFIG nsjail config file path -

Python dependency preparation

Before debugging a Python script, its imports are installed through windmill prepare-deps, which runs uv without a database connection. It cannot read the instance settings, so it takes its registry configuration from the environment of the debug service instead, and the Python server is handed the resulting venv with --venv-path. The install runs in the service rather than in the session because a private index URL usually embeds credentials and the Python server executes the debugged script inside its own interpreter, where anything it holds is readable by that script.

Set these on the debug service. Where two names are listed the first wins; a worker reads the PIP_* / PY_* names in the same way, except for the index URLs, whose worker env fallbacks are only PIP_INDEX_URL / PIP_EXTRA_INDEX_URL (the PY_* spellings are accepted here for symmetry with the other settings):

Variable Description Default
PY_INDEX_URL / PIP_INDEX_URL Package index (--index-url) PyPI
PY_EXTRA_INDEX_URL / PIP_EXTRA_INDEX_URL Extra indexes, comma-separated (--extra-index-url) -
PY_TRUSTED_HOST / PIP_TRUSTED_HOST Hosts to trust, whitespace-separated (--trusted-host) -
PY_INDEX_CERT / PIP_INDEX_CERT CA bundle for the index, passed to uv as SSL_CERT_FILE. Falls back to SSL_CERT_FILE, then REQUESTS_CA_BUNDLE, then CURL_CA_BUNDLE, so a host that configures its CA under any of those names is picked up. Whichever is used replaces uv's own roots rather than adding to them, so it has to be a complete bundle: one holding only a private CA leaves every public index untrusted. bun install gets the same bundle as NODE_EXTRA_CA_CERTS, the only spelling Bun reads -
SSL_CERT_DIR Directory of certificates, forwarded to uv as-is. Replaces uv's roots the same way the bundle does, so a directory holding only a private CA leaves public indexes untrusted -
PY_NATIVE_CERT / UV_NATIVE_TLS true to also trust the platform certificate store (--native-tls) false
UV_INDEX_STRATEGY uv index strategy unsafe-best-match
UV_HTTP_TIMEOUT uv HTTP request timeout, in seconds uv's own default
DAP_PREPARE_DEPS_TIMEOUT_MS How long to wait for the install before starting the session without it 120000

When the install fails, the CLI answers success: false and carries the installer's stderr in both error and install_stderr; the service reports it to the client as an output event, so the reason (unreachable mirror, untrusted certificate, unknown package) reaches the user instead of a bare ModuleNotFoundError at the first import.

Proxy variables (HTTP_PROXY / HTTPS_PROXY / NO_PROXY, in either case) are forwarded from the service into each session, since the debugged script needs them for its own outbound calls, exactly as a job's script does on a worker. When a proxy is set without a bypass list, NO_PROXY defaults to localhost,127.0.0.1 so calls to BASE_INTERNAL_URL are not proxied.

Trust roots are forwarded alongside them: SSL_CERT_FILE, SSL_CERT_DIR, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE and NODE_EXTRA_CA_CERTS. Behind a TLS-intercepting proxy these are what let the debugged script's own HTTPS calls verify, and installing the CA in the container's system store is not enough on its own, since requests carries its own bundle and Node reads only NODE_EXTRA_CA_CERTS. Registry settings are deliberately not forwarded: they carry credentials and only the service needs them.

To install that CA into the container's system store in the first place, set INIT_SCRIPT on the windmill_extra container (e.g. INIT_SCRIPT=update-ca-certificates). It runs before any service starts and aborts startup if it fails, the same hook a worker offers.

Keeping the settings out of the session's environment only bounds what the debugged script can read from itself. An unsandboxed session runs under the same user as the service and can still read the service's environment through /proc, the same way a job can read a worker's when the worker runs unsandboxed. Isolating sessions from the service takes --nsjail --nsjail-config nsjail.debug.config.proto: it is that config's PID namespace and mount_proc that put the service out of reach, not the flag on its own.

Frontend Integration

<script>
  import { MonacoDebugger } from './debug'
  let editor // Monaco editor instance
  let code = 'print("Hello")'
</script>

<MonacoDebugger {editor} {code} language="python3" />

Testing

# Test Python debugger
bun run debug/test_dap_server.py

# Test Bun debugger
bun run debug/test_dap_server_bun.ts

# Test unified service
bun run debug/test_debug_service.ts