detrix-py 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ **target**
2
+ .cargo/
3
+ .claude/
4
+ logs/
5
+ fixtures/go/detrix_example_app
6
+
7
+ # Go client built binaries
8
+ clients/go/basic_usage
9
+ clients/go/test_wake
10
+ clients/go/trade_bot
11
+ fixtures/go/go
@@ -0,0 +1,282 @@
1
+ # Python Client Architecture
2
+
3
+ This document describes the architecture of the Detrix Python client.
4
+
5
+ ## Module Structure
6
+
7
+ ```
8
+ src/detrix/
9
+ ├── __init__.py # Public API: init, status, wake, sleep, shutdown
10
+ ├── _operations.py # Core wake/sleep operations (shared by __init__ and control)
11
+ ├── _state.py # Global state singleton with thread-safe access
12
+ ├── auth.py # Token discovery (env var, file)
13
+ ├── config.py # Configuration utilities (ports, names, env vars)
14
+ ├── control.py # HTTP control plane server
15
+ ├── daemon.py # Daemon client protocol and HTTP implementation
16
+ ├── debugger.py # debugpy lifecycle management
17
+ ├── errors.py # Error type hierarchy
18
+ └── py.typed # PEP 561 marker for type checking
19
+ ```
20
+
21
+ ## Module Dependencies
22
+
23
+ ```
24
+ ┌─────────────────┐
25
+ │ __init__.py │ ← Public API (delegates to _operations)
26
+ │ control.py │ ← HTTP server (delegates to _operations)
27
+ ├─────────────────┤
28
+ │ _operations.py │ ← Core wake/sleep logic (shared)
29
+ ├─────────────────┤
30
+ │ daemon.py │ ← Daemon communication
31
+ │ debugger.py │ ← debugpy management
32
+ ├─────────────────┤
33
+ │ _state.py │ ← Global state singleton
34
+ │ auth.py │ ← Token handling
35
+ │ config.py │ ← Configuration
36
+ │ errors.py │ ← Error types
37
+ └─────────────────┘
38
+ ```
39
+
40
+ **Dependency Rules:**
41
+ - `__init__.py` imports from `_operations`, `_state`, `config`, `control`, `daemon`, `debugger`, `errors`
42
+ - `control.py` imports from `_operations`, `_state`, `auth`, `debugger`, `errors`
43
+ - `_operations.py` imports from `_state`, `auth`, `daemon`, `debugger`, `errors`
44
+ - `daemon.py` imports only from `errors`
45
+ - `debugger.py` has no local imports (only stdlib + debugpy)
46
+ - `_state.py`, `auth.py`, `config.py`, `errors.py` have no local imports
47
+
48
+ ## State Machine
49
+
50
+ ```
51
+ ┌──────────┐ ┌────────┐ success ┌───────┐
52
+ │ SLEEPING │ ──────────► │ WAKING │ ───────────► │ AWAKE │
53
+ └──────────┘ wake() └────────┘ └───────┘
54
+ ▲ │ │
55
+ │ │ failure │
56
+ │ └──────────────────────┤
57
+ │ │
58
+ │ sleep() │
59
+ └─────────────────────────────────────────────────┘
60
+ ```
61
+
62
+ **Note:** WAKING is a transitional state used internally during `wake()` to allow
63
+ `status()` calls while network I/O is in progress. It is not exposed in the public API.
64
+
65
+ ### States
66
+
67
+ | State | debugpy Loaded | debugpy Listening | Registered with Daemon |
68
+ |----------|----------------|-------------------|------------------------|
69
+ | SLEEPING | No | No* | No |
70
+ | WAKING | Yes | In progress | In progress |
71
+ | AWAKE | Yes | Yes | Yes |
72
+
73
+ *Note: After first wake, debugpy port remains open due to debugpy limitation.
74
+ The `debug_port_active` field tracks the actual port state.
75
+
76
+ **WAKING** is a transitional state that exists only during the `wake()` call while
77
+ network I/O is in progress. This allows `status()` to return immediately without
78
+ blocking on daemon communication.
79
+
80
+ ### Transitions
81
+
82
+ | From | To | Trigger | Actions |
83
+ |----------|----------|----------------|--------------------------------------------|
84
+ | SLEEPING | WAKING | wake() | Load debugpy, mark transitional |
85
+ | WAKING | AWAKE | wake() success | Start listener, register with daemon |
86
+ | WAKING | SLEEPING | wake() failure | Revert to sleeping state |
87
+ | AWAKE | SLEEPING | sleep() | Unregister, mark state (port stays open) |
88
+
89
+ ## Thread Safety
90
+
91
+ ### Global State Access
92
+
93
+ All state access must use the `ClientState.lock` (RLock):
94
+
95
+ ```python
96
+ state = get_state()
97
+ with state.lock:
98
+ # Read or modify state fields
99
+ state.state = State.AWAKE
100
+ state.connection_id = "..."
101
+ ```
102
+
103
+ The lock is reentrant to allow nested acquisitions in the same thread.
104
+
105
+ ### Lock-Free Pattern for Network I/O
106
+
107
+ The `wake()` and `sleep()` functions use a three-phase lock-free pattern to
108
+ prevent `status()` calls from blocking during network I/O:
109
+
110
+ ```python
111
+ # Phase 1: Read state (short lock)
112
+ with state.lock:
113
+ if state.state == State.AWAKE:
114
+ return already_awake_result
115
+ previous_state = state.state
116
+ state.state = State.WAKING # Transitional state
117
+ # Capture config for use outside lock
118
+ daemon_url = state.daemon_url
119
+ ...
120
+
121
+ # Phase 2: Network I/O (no lock held)
122
+ # status() can return immediately while this runs
123
+ check_daemon_health(daemon_url)
124
+ register_connection(...)
125
+
126
+ # Phase 3: Update state (short lock)
127
+ with state.lock:
128
+ state.state = State.AWAKE
129
+ state.connection_id = connection_id
130
+ ```
131
+
132
+ ### Wake Lock
133
+
134
+ A separate `wake_lock` (non-reentrant Lock) prevents concurrent `wake()` calls:
135
+
136
+ ```python
137
+ if not state.wake_lock.acquire(blocking=False):
138
+ # Another thread is waking - wait for it
139
+ with state.wake_lock:
140
+ pass
141
+ # Check result and return
142
+ ```
143
+
144
+ This ensures only one wake operation runs at a time, while allowing `status()`
145
+ and other read operations to proceed without blocking.
146
+
147
+ ### Control Plane Thread
148
+
149
+ The HTTP control plane runs in a daemon thread. Request handlers access the
150
+ global state through the lock. The daemon thread flag ensures the thread
151
+ terminates when the main process exits.
152
+
153
+ ### Lock Acquisition Order
154
+
155
+ When multiple locks are needed:
156
+ 1. Try `wake_lock` first (non-blocking)
157
+ 2. Then acquire `ClientState.lock` briefly for state reads/writes
158
+ 3. Never hold any lock during network I/O
159
+
160
+ ## Error Hierarchy
161
+
162
+ ```
163
+ DetrixError (base)
164
+ ├── ConfigError # Configuration/initialization errors
165
+ ├── DaemonError # Communication with daemon failed
166
+ ├── DebuggerError # debugpy-related errors
167
+ └── ControlPlaneError # Control plane server errors
168
+ ```
169
+
170
+ `DaemonConnectionError` is kept as an alias for `DaemonError` for backward
171
+ compatibility.
172
+
173
+ ## Control Plane HTTP API
174
+
175
+ | Endpoint | Method | Auth Required | Description |
176
+ |-----------------|--------|---------------|----------------------------|
177
+ | /detrix/health | GET | No | Health check |
178
+ | /detrix/status | GET | Remote only | Get current status |
179
+ | /detrix/info | GET | Remote only | Get process info |
180
+ | /detrix/wake | POST | Remote only | Start debugger + register |
181
+ | /detrix/sleep | POST | Remote only | Unregister connection |
182
+
183
+ ### Authentication
184
+
185
+ - Localhost requests (127.0.0.1, ::1, localhost, 0.0.0.0) are always allowed
186
+ - Remote requests require Bearer token matching DETRIX_TOKEN or ~/detrix/mcp-token
187
+ - If no token is configured, remote requests are DENIED (security-first default)
188
+
189
+ ## Known Limitations
190
+
191
+ ### 1. debugpy Cannot Stop Listener
192
+
193
+ **Symptom:** After `sleep()`, the debug port remains open.
194
+
195
+ **Cause:** debugpy does not support stopping its listener once started. The
196
+ `listen()` function can only be called once per process.
197
+
198
+ **Impact:**
199
+ - `debug_port_active` remains True after sleep
200
+ - Same port is reused on subsequent wake calls
201
+ - Port is freed only when process exits
202
+
203
+ **Reference:** https://github.com/microsoft/debugpy/issues/895
204
+
205
+ ### 2. Port Allocation Race Condition (TOCTOU) - FIXED
206
+
207
+ **Previous Issue:** `get_free_port()` found a free port, closed the socket, then
208
+ passed the port to debugpy. Another process could grab the port in between.
209
+
210
+ **Fix:** We now pass `debug_port=0` directly to `debugpy.listen()`, which handles
211
+ port allocation atomically. The port is assigned by the OS at bind time, with no
212
+ window for another process to claim it.
213
+
214
+ **Note:** `get_free_port()` still exists in `config.py` for other use cases,
215
+ but is no longer used in the wake path.
216
+
217
+ ### 3. Control Server Thread Termination
218
+
219
+ **Symptom:** Control server thread may not terminate immediately on shutdown.
220
+
221
+ **Cause:** HTTP server shutdown is cooperative. If a request is in progress,
222
+ the thread waits for it to complete.
223
+
224
+ **Impact:** Warning logged if thread doesn't terminate within timeout (default 2s).
225
+ Thread is marked as daemon, so it won't prevent process exit.
226
+
227
+ ## Configuration Priority
228
+
229
+ Configuration values are resolved in this order (first wins):
230
+ 1. Explicit parameters to `init()`
231
+ 2. Environment variables (DETRIX_*)
232
+ 3. Default values
233
+
234
+ | Parameter | Env Variable | Default |
235
+ |-----------------------|-------------------------------|-------------------------|
236
+ | name | DETRIX_NAME | "detrix-client-{pid}" |
237
+ | control_host | DETRIX_CONTROL_HOST | "127.0.0.1" |
238
+ | control_port | DETRIX_CONTROL_PORT | 0 (auto-assign) |
239
+ | debug_port | DETRIX_DEBUG_PORT | 0 (auto-assign) |
240
+ | daemon_url | DETRIX_DAEMON_URL | "http://127.0.0.1:8090" |
241
+ | token | DETRIX_TOKEN | (from file) |
242
+ | detrix_home | DETRIX_HOME | ~/detrix |
243
+ | health_check_timeout | DETRIX_HEALTH_CHECK_TIMEOUT | 2.0 seconds |
244
+ | register_timeout | DETRIX_REGISTER_TIMEOUT | 5.0 seconds |
245
+ | unregister_timeout | DETRIX_UNREGISTER_TIMEOUT | 2.0 seconds |
246
+
247
+ ## DaemonClient Protocol
248
+
249
+ The `DaemonClient` protocol enables dependency injection and testing:
250
+
251
+ ```python
252
+ class DaemonClient(Protocol):
253
+ def health_check(self, timeout: float = 2.0) -> bool: ...
254
+ def register(self, host: str, port: int, connection_id: str,
255
+ token: Optional[str] = None, timeout: float = 5.0) -> str: ...
256
+ def unregister(self, connection_id: str,
257
+ token: Optional[str] = None, timeout: float = 2.0) -> None: ...
258
+ ```
259
+
260
+ `HttpDaemonClient` is the default implementation using httpx with connection pooling.
261
+
262
+ ## Go/Rust Implementation Notes
263
+
264
+ When implementing clients in Go or Rust:
265
+
266
+ 1. **Singleton Pattern:** Use package-level state with mutex protection
267
+ - Go: `sync.RWMutex` protecting a struct
268
+ - Rust: `OnceCell<RwLock<ClientState>>`
269
+
270
+ 2. **Error Handling:** Implement equivalent error types with the same hierarchy
271
+
272
+ 3. **HTTP Client:** Use native HTTP clients (net/http, reqwest) implementing
273
+ the same endpoints and error handling
274
+
275
+ 4. **Debug Adapter:** Use language-appropriate DAP libraries (delve for Go,
276
+ lldb-dap/CodeLLDB for Rust)
277
+
278
+ 5. **Thread Safety:** Use native synchronization primitives matching the Python
279
+ RLock semantics (reentrant mutex)
280
+
281
+ 6. **Known Limitations:** Document the same limitations - debug adapter port
282
+ persistence is a common issue across DAP implementations
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ilya Dyachenko https://github.com/flashus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,337 @@
1
+ Metadata-Version: 2.4
2
+ Name: detrix-py
3
+ Version: 1.0.0
4
+ Summary: Python client for Detrix - debug-on-demand observability with zero overhead
5
+ Project-URL: Homepage, https://github.com/flashus/detrix
6
+ Project-URL: Documentation, https://github.com/flashus/detrix/tree/main/clients/python
7
+ Project-URL: Repository, https://github.com/flashus/detrix
8
+ Author: Ilya Dyachenko
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: dap,debugging,debugpy,metrics,observability
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Debuggers
21
+ Classifier: Topic :: System :: Monitoring
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: debugpy>=1.8.0
25
+ Requires-Dist: httpx>=0.28.1
26
+ Provides-Extra: dev
27
+ Requires-Dist: datamodel-code-generator>=0.25.0; extra == 'dev'
28
+ Requires-Dist: mypy>=1.8.0; extra == 'dev'
29
+ Requires-Dist: pytest-asyncio>=1.3.0; extra == 'dev'
30
+ Requires-Dist: pytest>=9.0.2; extra == 'dev'
31
+ Requires-Dist: ruff>=0.2.0; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # Detrix Python Client
35
+
36
+ Debug-on-demand observability for Python applications with zero overhead when inactive.
37
+
38
+ ## Overview
39
+
40
+ The Detrix Python client enables your application to be observed by the [Detrix](https://github.com/flashus/detrix) daemon without:
41
+
42
+ - **Code modifications** - No print statements or logging changes needed
43
+ - **Redeployment** - Add metrics to running processes
44
+ - **Performance overhead** - Zero cost when not actively observing
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ # Using uv (recommended)
50
+ uv add detrix-py
51
+
52
+ # Using pip
53
+ pip install detrix-py
54
+ ```
55
+
56
+ ## Quick Start
57
+
58
+ ### 1. Start the Detrix daemon
59
+
60
+ ```bash
61
+ detrix serve --daemon
62
+ ```
63
+
64
+ ### 2. Initialize the client in your application
65
+
66
+ ```python
67
+ import detrix
68
+
69
+ # Initialize client - starts control plane, stays SLEEPING (zero overhead)
70
+ detrix.init(
71
+ name="my-service",
72
+ daemon_url="http://127.0.0.1:8090",
73
+ )
74
+
75
+ # Your application code runs normally...
76
+ def process_request(request):
77
+ data = transform(request)
78
+ return data
79
+ ```
80
+
81
+ ### 3. Enable observability when needed
82
+
83
+ ```python
84
+ # Wake up - starts debugger, registers with daemon
85
+ detrix.wake()
86
+
87
+ # Now the daemon can set observation points on any line
88
+ # No code changes needed!
89
+
90
+ # When done observing
91
+ detrix.sleep()
92
+ ```
93
+
94
+ ## Try It
95
+
96
+ Run the end-to-end example that simulates an AI agent: starts a sample app, wakes it, adds metrics, and captures events.
97
+
98
+ ```bash
99
+ # 1. Start the Detrix server
100
+ detrix serve --daemon
101
+
102
+ # 2. Run the agent simulation (from clients/python/)
103
+ uv run python examples/test_wake.py --daemon-port 8090
104
+ ```
105
+
106
+ Other examples in `examples/`:
107
+
108
+ | Example | Description | Run |
109
+ |---------|-------------|-----|
110
+ | `basic_usage.py` | Init / wake / sleep cycle | `uv run python examples/basic_usage.py` |
111
+ | `trade_bot_detrix.py` | Long-running app with embedded client | `uv run python examples/trade_bot_detrix.py` |
112
+ | `test_wake.py` | Agent simulation (starts app, wakes, observes) | `uv run python examples/test_wake.py` |
113
+
114
+ ## API Reference
115
+
116
+ ### `detrix.init(**kwargs)`
117
+
118
+ Initialize the client. Starts the control plane HTTP server.
119
+
120
+ **Parameters:**
121
+ - `name` (str): Connection name (default: `"detrix-client-{pid}"`)
122
+ - `control_host` (str): Control plane host (default: `"127.0.0.1"`)
123
+ - `control_port` (int): Control plane port (default: `0` = auto-assign)
124
+ - `debug_port` (int): debugpy port (default: `0` = auto-assign on wake)
125
+ - `daemon_url` (str): Detrix daemon URL (default: `"http://127.0.0.1:8090"`)
126
+ - `start_state` (str): Initial state `"sleeping"` or `"warm"` (default: `"sleeping"`)
127
+ - `detrix_home` (str): Path to Detrix home directory (default: `~/detrix`)
128
+ - `health_check_timeout` (float): Timeout for daemon health checks in seconds (default: `2.0`)
129
+ - `register_timeout` (float): Timeout for connection registration in seconds (default: `5.0`)
130
+ - `unregister_timeout` (float): Timeout for connection unregistration in seconds (default: `2.0`)
131
+
132
+ ### `detrix.status() -> dict`
133
+
134
+ Get current client status.
135
+
136
+ **Returns:**
137
+ ```python
138
+ {
139
+ "state": "sleeping" | "warm" | "awake",
140
+ "name": "my-service-12345",
141
+ "control_host": "127.0.0.1",
142
+ "control_port": 9000,
143
+ "debug_port": 5678, # 0 if never awake
144
+ "debug_port_active": True, # True if debug port is actually open
145
+ "daemon_url": "http://127.0.0.1:8090",
146
+ "connection_id": "my-service-12345", # None if not awake
147
+ }
148
+ ```
149
+
150
+ ### `detrix.wake(daemon_url: str = None) -> dict`
151
+
152
+ Start debugger and register with daemon.
153
+
154
+ **Parameters:**
155
+ - `daemon_url` (str): Override daemon URL (optional)
156
+
157
+ **Raises:**
158
+ - `DaemonError`: If daemon is not reachable or URL is invalid
159
+
160
+ **Returns:**
161
+ ```python
162
+ {"status": "awake", "debug_port": 5678, "connection_id": "my-service-12345"}
163
+ ```
164
+
165
+ ### `detrix.sleep() -> dict`
166
+
167
+ Stop debugger and unregister from daemon.
168
+
169
+ **Returns:**
170
+ ```python
171
+ {"status": "sleeping"}
172
+ ```
173
+
174
+ ### `detrix.shutdown()`
175
+
176
+ Stop control server and cleanup. Call `init()` again to reinitialize.
177
+
178
+ ## State Machine
179
+
180
+ ```
181
+ ┌──────────┐ wake() ┌───────┐
182
+ │ SLEEPING │ ──────────────────────►│ AWAKE │
183
+ └──────────┘ └───────┘
184
+ ▲ │
185
+ │ sleep() │
186
+ └────────────────────────────────────┘
187
+ ```
188
+
189
+ - **SLEEPING**: No debugger loaded, zero overhead
190
+ - **AWAKE**: debugpy listening, registered with daemon
191
+
192
+ ## Control Plane HTTP Endpoints
193
+
194
+ The client exposes a local HTTP server for management:
195
+
196
+ | Endpoint | Method | Description |
197
+ |----------|--------|-------------|
198
+ | `/detrix/health` | GET | Health check |
199
+ | `/detrix/status` | GET | Get current status |
200
+ | `/detrix/info` | GET | Get process info |
201
+ | `/detrix/wake` | POST | Start debugger + register |
202
+ | `/detrix/sleep` | POST | Stop debugger + unregister |
203
+
204
+ ## Environment Variables
205
+
206
+ | Variable | Description |
207
+ |---------------------------------|------------------------------------------|
208
+ | `DETRIX_NAME` | Default connection name |
209
+ | `DETRIX_CONTROL_HOST` | Control plane host |
210
+ | `DETRIX_CONTROL_PORT` | Control plane port |
211
+ | `DETRIX_DEBUG_PORT` | debugpy port |
212
+ | `DETRIX_DAEMON_URL` | Daemon URL |
213
+ | `DETRIX_TOKEN` | Authentication token |
214
+ | `DETRIX_HOME` | Detrix home directory |
215
+ | `DETRIX_HEALTH_CHECK_TIMEOUT` | Timeout for daemon health checks (secs) |
216
+ | `DETRIX_REGISTER_TIMEOUT` | Timeout for connection registration (secs)|
217
+ | `DETRIX_UNREGISTER_TIMEOUT` | Timeout for connection unregistration (secs)|
218
+
219
+ ## Security
220
+
221
+ ### Authentication
222
+
223
+ The control plane HTTP server has a security-first authentication model:
224
+
225
+ - **Localhost requests** (127.0.0.1, ::1, localhost) are always allowed without authentication
226
+ - **Remote requests** require a valid Bearer token
227
+
228
+ To enable remote access:
229
+ 1. Set `DETRIX_TOKEN` environment variable, or
230
+ 2. Create `~/detrix/mcp-token` file with the token
231
+
232
+ If no token is configured, remote requests are denied by default.
233
+
234
+ ### Remote Exposure Guidelines
235
+
236
+ The control plane is designed for **localhost access only** by default. If you need to expose it remotely:
237
+
238
+ 1. **Always configure authentication** - Set `DETRIX_TOKEN` or create `~/detrix/mcp-token`
239
+ 2. **Use a reverse proxy** - Place nginx, HAProxy, or similar in front for:
240
+ - TLS termination (HTTPS)
241
+ - Rate limiting
242
+ - Access logging
243
+ - IP allowlisting
244
+ 3. **Restrict network access** - Use firewall rules to limit which hosts can connect
245
+ 4. **Protect the token file** - Ensure `~/detrix/mcp-token` has restrictive permissions (`chmod 600`)
246
+
247
+ Example nginx configuration:
248
+ ```nginx
249
+ location /detrix/ {
250
+ # Rate limiting
251
+ limit_req zone=detrix burst=10 nodelay;
252
+
253
+ # Proxy to control plane
254
+ proxy_pass http://127.0.0.1:9000;
255
+ proxy_set_header Host $host;
256
+ proxy_set_header X-Real-IP $remote_addr;
257
+ }
258
+ ```
259
+
260
+ **Note:** The control plane exposes debugging capabilities. Unauthorized access could allow an attacker to inspect process state. Always treat it as a sensitive endpoint.
261
+
262
+ ## Error Handling
263
+
264
+ The client provides a hierarchy of exception types:
265
+
266
+ ```python
267
+ from detrix import DetrixError, ConfigError, DaemonError, DebuggerError
268
+
269
+ try:
270
+ detrix.wake()
271
+ except DaemonError as e:
272
+ print(f"Cannot reach daemon: {e}")
273
+ except DebuggerError as e:
274
+ print(f"Debugger error: {e}")
275
+ except DetrixError as e:
276
+ print(f"General error: {e}")
277
+ ```
278
+
279
+ - `DetrixError`: Base class for all Detrix errors
280
+ - `ConfigError`: Configuration/initialization errors
281
+ - `DaemonError`: Communication with daemon failed
282
+ - `DebuggerError`: debugpy-related errors
283
+ - `ControlPlaneError`: Control plane server errors
284
+
285
+ Note: `DaemonConnectionError` is kept as an alias for `DaemonError` for backward compatibility.
286
+
287
+ ## Known Limitations
288
+
289
+ ### 1. debugpy Port Remains Open After Sleep
290
+
291
+ **Issue:** After calling `sleep()`, the debug port remains open until the process exits.
292
+
293
+ **Cause:** This is a limitation of debugpy itself - it does not support stopping its listener once started. See [debugpy#895](https://github.com/microsoft/debugpy/issues/895).
294
+
295
+ **Impact:**
296
+ - The `debug_port_active` field in status remains `True` after sleep
297
+ - Calling `wake()` after `sleep()` reuses the same port
298
+ - The port is only freed when the process terminates
299
+
300
+ **Workaround:** This is expected behavior. If you need to fully release the port, you must restart the process.
301
+
302
+ ### 2. Port Allocation Race Condition (Fixed)
303
+
304
+ **Previous issue:** Another process could claim a port between detection and binding.
305
+
306
+ **Resolution:** The client now passes `port=0` directly to `debugpy.listen()`, which
307
+ handles port allocation atomically at bind time. There is no window for another
308
+ process to claim the port.
309
+
310
+ **Note:** If you explicitly specify a port via `debug_port` parameter or
311
+ `DETRIX_DEBUG_PORT`, the standard TOCTOU limitation applies (another process
312
+ could theoretically bind to that port first). Use `port=0` for guaranteed
313
+ atomic allocation.
314
+
315
+ ## Architecture
316
+
317
+ For detailed architecture documentation, see [ARCHITECTURE.md](ARCHITECTURE.md).
318
+
319
+ ## Development
320
+
321
+ ```bash
322
+ # Install dev dependencies
323
+ uv sync --all-extras
324
+
325
+ # Run tests
326
+ uv run pytest
327
+
328
+ # Type check
329
+ uv run mypy src/detrix
330
+
331
+ # Lint
332
+ uv run ruff check src/detrix
333
+ ```
334
+
335
+ ## License
336
+
337
+ MIT