hypha-debugger 0.1.8__tar.gz → 0.1.10__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.
Files changed (22) hide show
  1. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/PKG-INFO +1 -1
  2. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/__init__.py +1 -1
  3. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/services/filesystem.py +22 -34
  4. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/services/source.py +65 -79
  5. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger.egg-info/PKG-INFO +1 -1
  6. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/pyproject.toml +1 -1
  7. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/tests/test_services.py +28 -8
  8. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/README.md +0 -0
  9. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/__main__.py +0 -0
  10. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/debugger.py +0 -0
  11. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/services/__init__.py +0 -0
  12. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/services/execute.py +0 -0
  13. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/services/info.py +0 -0
  14. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/services/inspect_vars.py +0 -0
  15. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/utils/__init__.py +0 -0
  16. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/utils/env.py +0 -0
  17. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger.egg-info/SOURCES.txt +0 -0
  18. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger.egg-info/dependency_links.txt +0 -0
  19. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger.egg-info/entry_points.txt +0 -0
  20. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger.egg-info/requires.txt +0 -0
  21. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger.egg-info/top_level.txt +0 -0
  22. {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypha-debugger
3
- Version: 0.1.8
3
+ Version: 0.1.10
4
4
  Summary: Injectable debugger for Python processes and AI agents, powered by Hypha RPC
5
5
  Author: Amun AI AB
6
6
  License: MIT
@@ -12,5 +12,5 @@ Usage (sync):
12
12
 
13
13
  from hypha_debugger.debugger import start_debugger, start_debugger_sync, DebugSession
14
14
 
15
- __version__ = "0.1.8"
15
+ __version__ = "0.1.10"
16
16
  __all__ = ["start_debugger", "start_debugger_sync", "DebugSession", "__version__"]
@@ -1,4 +1,4 @@
1
- """File system browsing and writing service (sandboxed to CWD)."""
1
+ """File system browsing and writing service."""
2
2
 
3
3
  import os
4
4
 
@@ -10,19 +10,19 @@ try:
10
10
  def list_files(
11
11
  path: str = Field(
12
12
  default=".",
13
- description='Directory path relative to CWD. Default: "." (current directory).',
13
+ description='Directory path (absolute or relative to CWD). Default: ".".',
14
14
  ),
15
15
  pattern: str = Field(
16
16
  default="",
17
17
  description='Glob pattern to filter files, e.g. "*.py".',
18
18
  ),
19
19
  ) -> dict:
20
- """List files and directories in a path relative to the process CWD."""
20
+ """List files and directories at the given path."""
21
21
  return _list_files_impl(path, pattern)
22
22
 
23
23
  @schema_function
24
24
  def read_file(
25
- path: str = Field(..., description="File path relative to CWD."),
25
+ path: str = Field(..., description="File path (absolute or relative to CWD)."),
26
26
  max_lines: int = Field(
27
27
  default=500,
28
28
  description="Maximum number of lines to read. Default: 500.",
@@ -36,7 +36,7 @@ try:
36
36
  description="File encoding. Default: utf-8.",
37
37
  ),
38
38
  ) -> dict:
39
- """Read a file relative to the process CWD. Returns the content as a string.
39
+ """Read a file. Returns the content as a string.
40
40
 
41
41
  Use offset and max_lines to paginate through large files.
42
42
  """
@@ -44,7 +44,7 @@ try:
44
44
 
45
45
  @schema_function
46
46
  def write_file(
47
- path: str = Field(..., description="File path relative to CWD."),
47
+ path: str = Field(..., description="File path (absolute or relative to CWD)."),
48
48
  content: str = Field(..., description="Content to write to the file."),
49
49
  mode: str = Field(
50
50
  default="w",
@@ -59,47 +59,39 @@ try:
59
59
  description="File encoding. Default: utf-8.",
60
60
  ),
61
61
  ) -> dict:
62
- """Write content to a file relative to the process CWD.
62
+ """Write content to a file.
63
63
 
64
64
  Creates parent directories automatically. Use mode="a" to append.
65
- Sandboxed: cannot write outside the process CWD.
66
65
  """
67
66
  return _write_file_impl(path, content, mode, create_dirs, encoding)
68
67
 
69
68
  except ImportError:
70
69
 
71
70
  def list_files(path: str = ".", pattern: str = "") -> dict:
72
- """List files and directories in a path relative to CWD."""
71
+ """List files and directories at the given path."""
73
72
  return _list_files_impl(path, pattern)
74
73
 
75
74
  def read_file(
76
75
  path: str = "", max_lines: int = 500, offset: int = 0, encoding: str = "utf-8"
77
76
  ) -> dict:
78
- """Read a file relative to CWD."""
77
+ """Read a file."""
79
78
  return _read_file_impl(path, max_lines, offset, encoding)
80
79
 
81
80
  def write_file(
82
81
  path: str = "", content: str = "", mode: str = "w",
83
82
  create_dirs: bool = True, encoding: str = "utf-8"
84
83
  ) -> dict:
85
- """Write content to a file relative to CWD."""
84
+ """Write content to a file."""
86
85
  return _write_file_impl(path, content, mode, create_dirs, encoding)
87
86
 
88
87
 
89
- def _resolve_safe(path: str) -> str:
90
- """Resolve path and ensure it's within CWD."""
91
- cwd = os.path.realpath(os.getcwd())
92
- resolved = os.path.realpath(os.path.join(cwd, path))
93
- if not resolved.startswith(cwd):
94
- raise PermissionError(f"Access denied: path escapes CWD ({path})")
95
- return resolved
88
+ def _resolve(path: str) -> str:
89
+ """Resolve path to absolute."""
90
+ return os.path.realpath(os.path.expanduser(path))
96
91
 
97
92
 
98
93
  def _list_files_impl(path: str, pattern: str) -> dict:
99
- try:
100
- resolved = _resolve_safe(path)
101
- except PermissionError as e:
102
- return {"error": str(e)}
94
+ resolved = _resolve(path)
103
95
 
104
96
  if not os.path.isdir(resolved):
105
97
  return {"error": f"Not a directory: {path}"}
@@ -123,7 +115,7 @@ def _list_files_impl(path: str, pattern: str) -> dict:
123
115
  return {"error": f"Permission denied: {path}"}
124
116
 
125
117
  return {
126
- "path": os.path.relpath(resolved, os.getcwd()),
118
+ "path": resolved,
127
119
  "entries": entries[:500],
128
120
  "total": len(entries),
129
121
  }
@@ -144,10 +136,7 @@ def _entry_info(name: str, full_path: str) -> dict:
144
136
 
145
137
 
146
138
  def _read_file_impl(path: str, max_lines: int, offset: int, encoding: str) -> dict:
147
- try:
148
- resolved = _resolve_safe(path)
149
- except PermissionError as e:
150
- return {"error": str(e)}
139
+ resolved = _resolve(path)
151
140
 
152
141
  if not os.path.isfile(resolved):
153
142
  return {"error": f"Not a file: {path}"}
@@ -163,7 +152,7 @@ def _read_file_impl(path: str, max_lines: int, offset: int, encoding: str) -> di
163
152
  lines.append(line)
164
153
  content = "".join(lines)
165
154
  return {
166
- "path": os.path.relpath(resolved, os.getcwd()),
155
+ "path": resolved,
167
156
  "content": content,
168
157
  "lines_read": len(lines),
169
158
  "offset": offset,
@@ -179,20 +168,19 @@ def _write_file_impl(
179
168
  if mode not in ("w", "a"):
180
169
  return {"error": f'Invalid mode: {mode}. Use "w" or "a".'}
181
170
 
182
- try:
183
- resolved = _resolve_safe(path)
184
- except PermissionError as e:
185
- return {"error": str(e)}
171
+ resolved = _resolve(path)
186
172
 
187
173
  try:
188
174
  if create_dirs:
189
- os.makedirs(os.path.dirname(resolved), exist_ok=True)
175
+ parent = os.path.dirname(resolved)
176
+ if parent:
177
+ os.makedirs(parent, exist_ok=True)
190
178
 
191
179
  with open(resolved, mode, encoding=encoding) as f:
192
180
  f.write(content)
193
181
 
194
182
  return {
195
- "path": os.path.relpath(resolved, os.getcwd()),
183
+ "path": resolved,
196
184
  "bytes_written": len(content.encode(encoding)),
197
185
  "mode": mode,
198
186
  }
@@ -87,104 +87,90 @@ def _get_skill_md_impl() -> str:
87
87
  return """# Hypha Remote Debugger — Python Process API
88
88
 
89
89
  ## Overview
90
- A debugger is attached to a running Python process. You can remotely execute code,
91
- read/write files, inspect variables, and query process info via the HTTP API.
90
+ A remote debugger is attached to a running Python process. You have full access
91
+ to execute code, read/write files, inspect variables, and install packages.
92
92
 
93
- ## Functions
93
+ **Recommended approach**: Use `execute_code` as your primary tool — it's a
94
+ persistent Python REPL where variables, imports, and functions survive across
95
+ calls. The other endpoints are convenience shortcuts.
94
96
 
95
- ### get_process_info()
96
- Get information about the current Python process.
97
- - **Returns**: `{pid, cwd, python_version, hostname, platform, memory_mb, cpu_count, ...}`
98
- - **Example**: `curl "$SERVICE_URL/get_process_info"`
97
+ ## Quick Start
99
98
 
100
- ### execute_code(code, namespace?, timeout?)
101
- Execute arbitrary Python code in the process. Uses AST parsing to automatically
102
- capture the last expression's value. Variables, functions, and imports persist
103
- across calls in the default REPL namespace.
104
- - **code** (string, required): Python code to execute.
105
- - **namespace** (string, default `""`): Namespace. Empty = persistent REPL. `"__main__"` = main module.
106
- - **timeout** (int, default `30`): Timeout in seconds. 0 for no timeout.
107
- - **Returns**: `{ok, stdout, stderr, result, result_repr, result_type, error?, error_type?, error_message?, traceback?, timed_out?}`
108
- - The last expression in the code block is automatically captured as `result`.
109
- E.g. `"x = 1\\nx + 1"` returns `result=2`.
110
- - **Example**:
111
- ```bash
112
- curl -X POST "$SERVICE_URL/execute_code" \\
113
- -H "Content-Type: application/json" \\
114
- -d '{"code": "import sys; sys.version"}'
115
- ```
99
+ ```bash
100
+ # Set the service URL (provided when debugger starts)
101
+ SERVICE_URL="<your-service-url>"
116
102
 
117
- ### get_variable(name, namespace?)
118
- Inspect a variable by name.
119
- - **name** (string, required): Variable name.
120
- - **namespace** (string, default `"__main__"`): Module namespace.
121
- - **Returns**: `{name, type, repr, length?, shape?, dtype?, keys?}`
103
+ # Run Python code (persistent REPL — variables survive across calls):
104
+ curl -X POST "$SERVICE_URL/execute_code" \\
105
+ -H "Content-Type: application/json" \\
106
+ -d '{"code": "import os; os.listdir(\\".\\")"}'
122
107
 
123
- ### list_variables(namespace?, filter?, include_private?)
124
- List variables in a namespace.
125
- - **namespace** (string, default `"__main__"`): Module namespace.
126
- - **filter** (string): Substring filter for names.
127
- - **include_private** (bool, default `false`): Include `_`-prefixed names.
128
- - **Returns**: List of `{name, type, repr}`.
108
+ # Install a package:
109
+ curl -X POST "$SERVICE_URL/execute_code" \\
110
+ -H "Content-Type: application/json" \\
111
+ -d '{"code": "import subprocess; subprocess.check_output([\\\"pip\\\", \\\"install\\\", \\\"requests\\\"])"}'
129
112
 
130
- ### get_stack_trace()
131
- Get stack traces of all threads.
132
- - **Returns**: List of `{thread_id, thread_name, stack}`.
133
- - **Example**: `curl "$SERVICE_URL/get_stack_trace"`
113
+ # Write a file via execute_code (avoids JSON escaping issues):
114
+ curl -X POST "$SERVICE_URL/execute_code" \\
115
+ -H "Content-Type: application/json" \\
116
+ -d '{"code": "with open(\\\"hello.py\\\", \\\"w\\\") as f: f.write(\\\"print(42)\\\\n\\\")"}'
117
+ ```
118
+
119
+ ## Functions
120
+
121
+ ### execute_code(code, namespace?, timeout?) — PRIMARY
122
+ Execute Python code in a persistent REPL. The last expression is automatically
123
+ captured as the return value via AST parsing.
124
+ - **code** (str, required): Python code to execute.
125
+ - **namespace** (str, default `""`): Empty = persistent REPL, `"__main__"` = main module.
126
+ - **timeout** (int, default `30`): Seconds. 0 = no timeout.
127
+ - **Returns**: `{ok, stdout, stderr, result, result_repr, result_type, error?, error_type?, error_message?, traceback?, timed_out?}`
128
+ - Variables, functions, and imports persist across calls.
129
+ - Example: `"x = 1\\nx + 1"` returns `{ok: true, result: 2}`.
130
+
131
+ ### get_process_info()
132
+ PID, CWD, Python version, hostname, platform, memory usage, CPU count.
133
+ - **Example**: `curl "$SERVICE_URL/get_process_info"`
134
134
 
135
135
  ### list_files(path?, pattern?)
136
- List files and directories (sandboxed to CWD).
137
- - **path** (string, default `"."`): Directory path relative to CWD.
138
- - **pattern** (string): Glob filter, e.g. `"*.py"`.
136
+ List directory contents. Accepts absolute or relative paths.
137
+ - **path** (str, default `"."`), **pattern** (str): glob filter e.g. `"*.py"`.
139
138
  - **Returns**: `{path, entries: [{name, type, size?}], total}`
140
- - **Example**: `curl "$SERVICE_URL/list_files"`
141
139
 
142
140
  ### read_file(path, max_lines?, offset?, encoding?)
143
- Read a file (sandboxed to CWD).
144
- - **path** (string, required): File path relative to CWD.
145
- - **max_lines** (int, default `500`): Max lines to read.
146
- - **offset** (int, default `0`): Lines to skip from beginning.
141
+ Read a file. Accepts absolute or relative paths.
142
+ - **path** (str, required), **max_lines** (int, default `500`), **offset** (int, default `0`).
147
143
  - **Returns**: `{path, content, lines_read, offset, truncated}`
148
- - **Example**:
149
- ```bash
150
- curl -X POST "$SERVICE_URL/read_file" \\
151
- -H "Content-Type: application/json" \\
152
- -d '{"path": "main.py"}'
153
- ```
154
144
 
155
145
  ### write_file(path, content, mode?, create_dirs?, encoding?)
156
- Write content to a file (sandboxed to CWD).
157
- - **path** (string, required): File path relative to CWD.
158
- - **content** (string, required): Content to write.
159
- - **mode** (string, default `"w"`): `"w"` to overwrite, `"a"` to append.
160
- - **create_dirs** (bool, default `true`): Create parent directories.
146
+ Write/append to a file. Auto-creates parent dirs. Accepts absolute or relative paths.
147
+ - **path** (str), **content** (str), **mode** (`"w"` or `"a"`).
161
148
  - **Returns**: `{path, bytes_written, mode}`
162
- - **Example**:
163
- ```bash
164
- curl -X POST "$SERVICE_URL/write_file" \\
165
- -H "Content-Type: application/json" \\
166
- -d '{"path": "hello.txt", "content": "Hello!"}'
167
- ```
149
+
150
+ ### get_variable(name, namespace?)
151
+ Inspect a variable by name in a module namespace.
152
+ - **Returns**: `{name, type, repr, length?, shape?, dtype?, keys?}`
153
+
154
+ ### list_variables(namespace?, filter?, include_private?)
155
+ List variables in a namespace. Filters by substring.
156
+
157
+ ### get_stack_trace()
158
+ Stack traces of all threads. Useful for debugging hangs.
168
159
 
169
160
  ### get_installed_packages(filter?)
170
- List installed pip packages.
171
- - **filter** (string): Substring filter for package names.
172
- - **Returns**: List of `{name, version}`.
173
- - **Example**: `curl "$SERVICE_URL/get_installed_packages"`
161
+ List pip packages. Optional name substring filter.
174
162
 
175
163
  ### get_source(module?)
176
- Get the source code of debugger modules.
177
- - **module** (string): Module path, e.g. `"services.execute"`. Empty to list modules.
178
- - **Returns**: `{module, source, lines}` or `{modules, hint}`.
164
+ Get debugger source code. Empty = list modules, e.g. `"services.execute"`.
179
165
 
180
166
  ### get_skill_md()
181
- Get this API documentation as Markdown.
182
- - **Returns**: This document as a string.
183
- - **Example**: `curl "$SERVICE_URL/get_skill_md"`
184
-
185
- ## Notes
186
- - All file operations are sandboxed to the process CWD.
187
- - POST endpoints accept JSON body with parameter names as keys.
188
- - GET endpoints take no parameters (or use query params).
189
- - Code execution has a default 30s timeout to prevent hangs.
167
+ Returns this document.
168
+
169
+ ## Tips
170
+ - **Use `execute_code` for file writes** when content has special characters —
171
+ it avoids JSON/curl escaping issues that affect `write_file` via HTTP.
172
+ - File operations accept absolute paths or paths relative to the process CWD.
173
+ - POST endpoints accept JSON body. GET endpoints take no body.
174
+ - Code execution has a 30s default timeout to prevent hangs.
175
+ - The REPL namespace is independent from `__main__` by default.
190
176
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypha-debugger
3
- Version: 0.1.8
3
+ Version: 0.1.10
4
4
  Summary: Injectable debugger for Python processes and AI agents, powered by Hypha RPC
5
5
  Author: Amun AI AB
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hypha-debugger"
7
- version = "0.1.8"
7
+ version = "0.1.10"
8
8
  description = "Injectable debugger for Python processes and AI agents, powered by Hypha RPC"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -185,10 +185,13 @@ def test_read_file_with_offset():
185
185
  assert result["lines_read"] <= 2
186
186
 
187
187
 
188
- def test_read_file_sandbox():
189
- result = read_file("../../etc/passwd")
190
- assert "error" in result
191
- assert "Access denied" in result["error"] or "Not a file" in result["error"]
188
+ def test_read_file_absolute():
189
+ """Can read files by absolute path."""
190
+ import pathlib
191
+ toml_path = str(pathlib.Path("pyproject.toml").resolve())
192
+ result = read_file(toml_path)
193
+ assert result.get("error") is None
194
+ assert "hypha-debugger" in result["content"]
192
195
 
193
196
 
194
197
  def test_write_file():
@@ -234,10 +237,27 @@ def test_write_file_create_dirs():
234
237
  os.chdir(old_cwd)
235
238
 
236
239
 
237
- def test_write_file_sandbox():
238
- result = write_file("../../etc/evil.txt", "pwned")
239
- assert "error" in result
240
- assert "Access denied" in result["error"]
240
+ def test_write_file_absolute():
241
+ """Can write files by absolute path."""
242
+ with tempfile.TemporaryDirectory() as tmpdir:
243
+ abs_path = os.path.join(tmpdir, "abs_test.txt")
244
+ result = write_file(abs_path, "absolute write")
245
+ assert result.get("error") is None
246
+ assert result["bytes_written"] == 14
247
+ read_result = read_file(abs_path)
248
+ assert read_result["content"] == "absolute write"
249
+
250
+
251
+ def test_list_files_absolute():
252
+ """Can list files by absolute path."""
253
+ with tempfile.TemporaryDirectory() as tmpdir:
254
+ # Create a test file
255
+ with open(os.path.join(tmpdir, "hello.txt"), "w") as f:
256
+ f.write("hi")
257
+ result = list_files(tmpdir)
258
+ assert result.get("error") is None
259
+ names = [e["name"] for e in result["entries"]]
260
+ assert "hello.txt" in names
241
261
 
242
262
 
243
263
  def test_write_file_invalid_mode():