* fix(debugger): return PrepareResult when the service prepared the venv `prepare_dependencies()` returns `PrepareResult` on every path except the service-prepared short-circuit, which returned the venv path as a bare `str`. `handle_launch` reads `prepared.error` on it, so every Python session whose script has a third-party import raised `AttributeError`, hung, and failed at 180s with `Debugpy command timeout: launch`. The two consumers of a prepare-deps response also read a `stderr` key the CLI does not emit; the field is `install_stderr`, and it carries the same text `error` already wraps in a sentence, so take one rather than joining both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(debugger): keep the failing step in the launch message, drop the dead branch Preferring the raw `install_stderr` made `_first_line` pick uv's opening progress line, so a refused launch reported "Using Python 3.12.13 environment at: venv" — which reads like success. `error` is the same text prefixed with the step that failed, so it is the better of the two to condense. The installer-diagnostics pass over a `success: true` response is unreachable: every `success: true` site in prepare_deps.rs sets `install_stderr: None`, and its comment claimed the opposite of what that file documents. It existed to work around a producer that warned and returned success on a failed `uv pip install`; that producer now returns `success: false`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 |
- |
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.
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