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.
- detrix_py-1.0.0/.gitignore +11 -0
- detrix_py-1.0.0/ARCHITECTURE.md +282 -0
- detrix_py-1.0.0/LICENSE +21 -0
- detrix_py-1.0.0/PKG-INFO +337 -0
- detrix_py-1.0.0/README.md +304 -0
- detrix_py-1.0.0/detrix/__init__.py +340 -0
- detrix_py-1.0.0/detrix/_generated/__init__.py +31 -0
- detrix_py-1.0.0/detrix/_generated/models.py +182 -0
- detrix_py-1.0.0/detrix/_operations.py +217 -0
- detrix_py-1.0.0/detrix/_state.py +92 -0
- detrix_py-1.0.0/detrix/auth.py +101 -0
- detrix_py-1.0.0/detrix/config.py +105 -0
- detrix_py-1.0.0/detrix/control.py +312 -0
- detrix_py-1.0.0/detrix/daemon.py +413 -0
- detrix_py-1.0.0/detrix/debugger.py +153 -0
- detrix_py-1.0.0/detrix/errors.py +83 -0
- detrix_py-1.0.0/detrix/py.typed +0 -0
- detrix_py-1.0.0/examples/basic_usage.py +88 -0
- detrix_py-1.0.0/examples/test_wake.py +417 -0
- detrix_py-1.0.0/examples/trade_bot_detrix.py +121 -0
- detrix_py-1.0.0/pyproject.toml +67 -0
- detrix_py-1.0.0/tests/__init__.py +1 -0
- detrix_py-1.0.0/tests/test_auth.py +94 -0
- detrix_py-1.0.0/tests/test_concurrent.py +330 -0
- detrix_py-1.0.0/tests/test_config.py +213 -0
- detrix_py-1.0.0/tests/test_control.py +114 -0
- detrix_py-1.0.0/tests/test_daemon.py +225 -0
- detrix_py-1.0.0/tests/test_edge_cases.py +420 -0
- detrix_py-1.0.0/tests/test_error_recovery.py +392 -0
- detrix_py-1.0.0/tests/test_integration.py +197 -0
- detrix_py-1.0.0/tests/test_state.py +86 -0
- detrix_py-1.0.0/uv.lock +956 -0
|
@@ -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
|
detrix_py-1.0.0/LICENSE
ADDED
|
@@ -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.
|
detrix_py-1.0.0/PKG-INFO
ADDED
|
@@ -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
|