hypha-debugger 0.1.4__tar.gz → 0.1.5__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.
- {hypha_debugger-0.1.4 → hypha_debugger-0.1.5}/PKG-INFO +1 -1
- {hypha_debugger-0.1.4 → hypha_debugger-0.1.5}/hypha_debugger/__init__.py +1 -1
- {hypha_debugger-0.1.4 → hypha_debugger-0.1.5}/hypha_debugger/__main__.py +3 -3
- {hypha_debugger-0.1.4 → hypha_debugger-0.1.5}/hypha_debugger/debugger.py +92 -67
- {hypha_debugger-0.1.4 → hypha_debugger-0.1.5}/hypha_debugger/services/execute.py +51 -14
- {hypha_debugger-0.1.4 → hypha_debugger-0.1.5}/hypha_debugger/services/filesystem.py +75 -7
- {hypha_debugger-0.1.4 → hypha_debugger-0.1.5}/hypha_debugger.egg-info/PKG-INFO +1 -1
- {hypha_debugger-0.1.4 → hypha_debugger-0.1.5}/pyproject.toml +1 -1
- hypha_debugger-0.1.5/tests/test_services.py +240 -0
- hypha_debugger-0.1.4/tests/test_services.py +0 -77
- {hypha_debugger-0.1.4 → hypha_debugger-0.1.5}/README.md +0 -0
- {hypha_debugger-0.1.4 → hypha_debugger-0.1.5}/hypha_debugger/services/__init__.py +0 -0
- {hypha_debugger-0.1.4 → hypha_debugger-0.1.5}/hypha_debugger/services/info.py +0 -0
- {hypha_debugger-0.1.4 → hypha_debugger-0.1.5}/hypha_debugger/services/inspect_vars.py +0 -0
- {hypha_debugger-0.1.4 → hypha_debugger-0.1.5}/hypha_debugger/utils/__init__.py +0 -0
- {hypha_debugger-0.1.4 → hypha_debugger-0.1.5}/hypha_debugger/utils/env.py +0 -0
- {hypha_debugger-0.1.4 → hypha_debugger-0.1.5}/hypha_debugger.egg-info/SOURCES.txt +0 -0
- {hypha_debugger-0.1.4 → hypha_debugger-0.1.5}/hypha_debugger.egg-info/dependency_links.txt +0 -0
- {hypha_debugger-0.1.4 → hypha_debugger-0.1.5}/hypha_debugger.egg-info/entry_points.txt +0 -0
- {hypha_debugger-0.1.4 → hypha_debugger-0.1.5}/hypha_debugger.egg-info/requires.txt +0 -0
- {hypha_debugger-0.1.4 → hypha_debugger-0.1.5}/hypha_debugger.egg-info/top_level.txt +0 -0
- {hypha_debugger-0.1.4 → hypha_debugger-0.1.5}/setup.cfg +0 -0
|
@@ -42,9 +42,9 @@ def main():
|
|
|
42
42
|
help="Human-readable service name (default: Python Debugger)",
|
|
43
43
|
)
|
|
44
44
|
parser.add_argument(
|
|
45
|
-
"--
|
|
45
|
+
"--require-token",
|
|
46
46
|
action="store_true",
|
|
47
|
-
help="
|
|
47
|
+
help="Require a JWT token for remote access (default: URL-secret mode, no token needed)",
|
|
48
48
|
)
|
|
49
49
|
args = parser.parse_args()
|
|
50
50
|
|
|
@@ -55,7 +55,7 @@ def main():
|
|
|
55
55
|
token=args.token,
|
|
56
56
|
service_id=args.service_id,
|
|
57
57
|
service_name=args.service_name,
|
|
58
|
-
require_token=
|
|
58
|
+
require_token=args.require_token,
|
|
59
59
|
)
|
|
60
60
|
|
|
61
61
|
print()
|
|
@@ -14,30 +14,29 @@ from hypha_debugger.services.inspect_vars import (
|
|
|
14
14
|
list_variables,
|
|
15
15
|
get_stack_trace,
|
|
16
16
|
)
|
|
17
|
-
from hypha_debugger.services.filesystem import list_files, read_file
|
|
17
|
+
from hypha_debugger.services.filesystem import list_files, read_file, write_file
|
|
18
18
|
|
|
19
19
|
logger = logging.getLogger("hypha_debugger")
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def _build_service_url(server_url: str, service_id: str) -> str:
|
|
23
|
-
"""Build
|
|
23
|
+
"""Build the HTTP service URL from server URL and full service ID.
|
|
24
24
|
|
|
25
25
|
Strips the clientId prefix so the URL uses only the bare service name.
|
|
26
|
-
Callers append ?_mode=last to resolve the most recent instance.
|
|
27
26
|
|
|
28
27
|
Args:
|
|
29
28
|
server_url: e.g. "https://hypha.aicell.io"
|
|
30
|
-
service_id: e.g. "ws-xxx/clientId:py-debugger"
|
|
29
|
+
service_id: e.g. "ws-xxx/clientId:py-debugger-abc123"
|
|
31
30
|
|
|
32
31
|
Returns:
|
|
33
|
-
e.g. "https://hypha.aicell.io/ws-xxx/services/py-debugger"
|
|
32
|
+
e.g. "https://hypha.aicell.io/ws-xxx/services/py-debugger-abc123"
|
|
34
33
|
"""
|
|
35
34
|
base = server_url.rstrip("/")
|
|
36
35
|
# service_id format: "workspace/clientId:svcName"
|
|
37
36
|
parts = service_id.split("/", 1)
|
|
38
37
|
if len(parts) == 2:
|
|
39
38
|
workspace, svc_part = parts
|
|
40
|
-
# Strip clientId: "abc123:py-debugger" → "py-debugger"
|
|
39
|
+
# Strip clientId: "abc123:py-debugger-xyz" → "py-debugger-xyz"
|
|
41
40
|
if ":" in svc_part:
|
|
42
41
|
svc_name = svc_part.split(":", 1)[1]
|
|
43
42
|
else:
|
|
@@ -46,76 +45,82 @@ def _build_service_url(server_url: str, service_id: str) -> str:
|
|
|
46
45
|
return f"{base}/services/{service_id}"
|
|
47
46
|
|
|
48
47
|
|
|
49
|
-
def _build_instruction_block(
|
|
50
|
-
service_url: str,
|
|
51
|
-
token: str,
|
|
52
|
-
require_token: bool = True,
|
|
53
|
-
) -> str:
|
|
48
|
+
def _build_instruction_block(service_url: str, token: str = "") -> str:
|
|
54
49
|
"""Build the instruction block with copy-paste commands for remote access.
|
|
55
50
|
|
|
56
|
-
Returns a formatted string
|
|
51
|
+
Returns a formatted string that can be pasted directly into an AI agent chat.
|
|
57
52
|
"""
|
|
58
|
-
|
|
53
|
+
auth = f' -H "Authorization: Bearer $TOKEN"' if token else ""
|
|
59
54
|
lines = [
|
|
60
55
|
"# Hypha Remote Debugger — Python Process",
|
|
61
56
|
"# A debugger is attached to a running Python process.",
|
|
62
|
-
"# You can remotely
|
|
57
|
+
"# You can remotely execute code, read/write files, inspect variables,",
|
|
63
58
|
"# and query process info via the HTTP API below.",
|
|
64
59
|
"#",
|
|
65
|
-
"# Available functions:
|
|
66
|
-
"#
|
|
60
|
+
"# Available functions:",
|
|
61
|
+
"# get_process_info - PID, Python version, CWD, platform, memory",
|
|
62
|
+
"# execute_code - Run arbitrary Python code (with timeout)",
|
|
63
|
+
"# get_variable - Inspect a variable by name",
|
|
64
|
+
"# list_variables - List variables in a namespace",
|
|
65
|
+
"# get_stack_trace - Stack traces of all threads",
|
|
66
|
+
"# list_files - List directory contents",
|
|
67
|
+
"# read_file - Read file content (with offset/limit)",
|
|
68
|
+
"# write_file - Write/append to a file",
|
|
69
|
+
"# get_installed_packages - List pip packages",
|
|
67
70
|
"#",
|
|
68
|
-
"#
|
|
71
|
+
"# POST endpoints accept JSON body with parameter names as keys.",
|
|
69
72
|
"",
|
|
70
73
|
f'SERVICE_URL="{service_url}"',
|
|
71
74
|
]
|
|
72
|
-
if
|
|
75
|
+
if token:
|
|
73
76
|
lines.append(f'TOKEN="{token}"')
|
|
74
77
|
lines += [
|
|
75
78
|
"",
|
|
76
79
|
"# Get process info (PID, Python version, CWD, memory):",
|
|
77
|
-
f'curl "$SERVICE_URL/get_process_info
|
|
80
|
+
f'curl "$SERVICE_URL/get_process_info"{auth}',
|
|
78
81
|
"",
|
|
79
|
-
"# Execute Python code
|
|
80
|
-
f
|
|
81
|
-
|
|
82
|
-
" -H \"Content-Type: application/json\""
|
|
82
|
+
"# Execute Python code (timeout default 30s):",
|
|
83
|
+
f'curl -X POST "$SERVICE_URL/execute_code"{auth}'
|
|
84
|
+
' -H "Content-Type: application/json"'
|
|
83
85
|
" -d '{\"code\": \"import sys; sys.version\"}'",
|
|
84
86
|
"",
|
|
85
|
-
"#
|
|
86
|
-
f
|
|
87
|
-
|
|
88
|
-
" -
|
|
89
|
-
" -d '{\"name\": \"my_var\"}'",
|
|
87
|
+
"# Write a file:",
|
|
88
|
+
f'curl -X POST "$SERVICE_URL/write_file"{auth}'
|
|
89
|
+
' -H "Content-Type: application/json"'
|
|
90
|
+
" -d '{\"path\": \"hello.txt\", \"content\": \"Hello, world!\"}'",
|
|
90
91
|
"",
|
|
91
92
|
"# List files in working directory:",
|
|
92
|
-
f'curl "$SERVICE_URL/list_files
|
|
93
|
+
f'curl "$SERVICE_URL/list_files"{auth}',
|
|
94
|
+
"",
|
|
95
|
+
"# Read a file:",
|
|
96
|
+
f'curl -X POST "$SERVICE_URL/read_file"{auth}'
|
|
97
|
+
' -H "Content-Type: application/json"'
|
|
98
|
+
" -d '{\"path\": \"hello.txt\"}'",
|
|
93
99
|
]
|
|
94
100
|
return "\n".join(lines)
|
|
95
101
|
|
|
96
102
|
|
|
97
103
|
def _print_session_info(
|
|
98
104
|
server_url: str,
|
|
99
|
-
service_id: str,
|
|
100
105
|
service_url: str,
|
|
101
|
-
token: str,
|
|
102
|
-
require_token: bool = True,
|
|
106
|
+
token: str = "",
|
|
103
107
|
) -> None:
|
|
104
108
|
"""Print session information with remote access URLs."""
|
|
105
109
|
print(f"[hypha-debugger] Connected to {server_url}")
|
|
106
|
-
print(f"[hypha-debugger] Service ID: {service_id}")
|
|
107
110
|
print(f"[hypha-debugger] Service URL: {service_url}")
|
|
108
|
-
if
|
|
111
|
+
if token:
|
|
109
112
|
print(f"[hypha-debugger] Token: {token}")
|
|
110
113
|
print()
|
|
111
114
|
sep = "=" * 60
|
|
112
|
-
print(
|
|
113
|
-
print("
|
|
114
|
-
print(
|
|
115
|
+
print(sep)
|
|
116
|
+
print(" WARNING: The URL below grants full access to this Python")
|
|
117
|
+
print(" process (execute code, read/write files). Only share it")
|
|
118
|
+
print(" with trusted agents or people.")
|
|
119
|
+
print(sep)
|
|
115
120
|
print()
|
|
116
|
-
print(_build_instruction_block(service_url, token
|
|
121
|
+
print(_build_instruction_block(service_url, token))
|
|
117
122
|
print()
|
|
118
|
-
print(
|
|
123
|
+
print(sep)
|
|
119
124
|
|
|
120
125
|
|
|
121
126
|
@dataclass
|
|
@@ -135,17 +140,17 @@ class DebugSession:
|
|
|
135
140
|
|
|
136
141
|
def print_instructions(self) -> str:
|
|
137
142
|
"""Print copy-paste instructions for remote access and return them."""
|
|
138
|
-
instructions = _build_instruction_block(
|
|
139
|
-
self.service_url, self.token, bool(self.token)
|
|
140
|
-
)
|
|
143
|
+
instructions = _build_instruction_block(self.service_url, self.token)
|
|
141
144
|
sep = "=" * 60
|
|
142
|
-
print(
|
|
143
|
-
print("
|
|
144
|
-
print(
|
|
145
|
+
print(sep)
|
|
146
|
+
print(" WARNING: The URL below grants full access to this Python")
|
|
147
|
+
print(" process (execute code, read/write files). Only share it")
|
|
148
|
+
print(" with trusted agents or people.")
|
|
149
|
+
print(sep)
|
|
145
150
|
print()
|
|
146
151
|
print(instructions)
|
|
147
152
|
print()
|
|
148
|
-
print(
|
|
153
|
+
print(sep)
|
|
149
154
|
return instructions
|
|
150
155
|
|
|
151
156
|
async def serve_forever(self):
|
|
@@ -181,6 +186,27 @@ class DebugSession:
|
|
|
181
186
|
self._thread.join(timeout=5)
|
|
182
187
|
|
|
183
188
|
|
|
189
|
+
def _resolve_id_and_visibility(
|
|
190
|
+
service_id: str,
|
|
191
|
+
visibility: str,
|
|
192
|
+
require_token: bool,
|
|
193
|
+
) -> tuple:
|
|
194
|
+
"""Resolve effective service ID and visibility.
|
|
195
|
+
|
|
196
|
+
By default (require_token=False), generates a unique random service ID
|
|
197
|
+
and registers as unlisted. The URL itself acts as the secret.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
(effective_id, effective_visibility)
|
|
201
|
+
"""
|
|
202
|
+
effective_id = service_id
|
|
203
|
+
# Always add a random suffix unless user provided a custom ID
|
|
204
|
+
if service_id == "py-debugger":
|
|
205
|
+
effective_id = f"py-debugger-{secrets.token_hex(16)}"
|
|
206
|
+
effective_visibility = visibility or ("protected" if require_token else "unlisted")
|
|
207
|
+
return effective_id, effective_visibility
|
|
208
|
+
|
|
209
|
+
|
|
184
210
|
async def start_debugger(
|
|
185
211
|
server_url: str,
|
|
186
212
|
workspace: str = "",
|
|
@@ -188,35 +214,34 @@ async def start_debugger(
|
|
|
188
214
|
service_id: str = "py-debugger",
|
|
189
215
|
service_name: str = "Python Debugger",
|
|
190
216
|
visibility: str = "",
|
|
191
|
-
require_token: bool =
|
|
217
|
+
require_token: bool = False,
|
|
192
218
|
) -> DebugSession:
|
|
193
219
|
"""Start the Hypha debugger (async).
|
|
194
220
|
|
|
195
221
|
Connects to a Hypha server and registers a debug service that remote
|
|
196
222
|
clients can use to inspect and interact with this Python process.
|
|
197
223
|
|
|
224
|
+
By default, the service is registered as "unlisted" with a unique random
|
|
225
|
+
service ID. The URL itself is unguessable — no token needed. Just keep
|
|
226
|
+
the URL secret.
|
|
227
|
+
|
|
198
228
|
Args:
|
|
199
229
|
server_url: Hypha server URL (required).
|
|
200
230
|
workspace: Workspace name (auto-assigned if empty).
|
|
201
231
|
token: Authentication token for connecting to Hypha.
|
|
202
|
-
service_id: Service ID to register as.
|
|
232
|
+
service_id: Service ID to register as. A random suffix is added by default.
|
|
203
233
|
service_name: Human-readable service name.
|
|
204
234
|
visibility: Service visibility override ("public", "protected", "unlisted").
|
|
205
|
-
|
|
206
|
-
require_token: Whether remote callers must supply a JWT token (default True).
|
|
207
|
-
When False, the service is registered as "unlisted" and the service ID
|
|
208
|
-
gets a random suffix — the URL itself acts as the secret, no token needed.
|
|
235
|
+
require_token: Whether to generate a JWT token for remote callers (default False).
|
|
209
236
|
|
|
210
237
|
Returns:
|
|
211
238
|
A DebugSession with service_id, workspace, server, service_url, and token.
|
|
212
239
|
"""
|
|
213
240
|
from hypha_rpc import connect_to_server
|
|
214
241
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
effective_id = f"py-debugger-{secrets.token_hex(8)}"
|
|
219
|
-
effective_visibility = visibility or ("protected" if require_token else "unlisted")
|
|
242
|
+
effective_id, effective_visibility = _resolve_id_and_visibility(
|
|
243
|
+
service_id, visibility, require_token
|
|
244
|
+
)
|
|
220
245
|
|
|
221
246
|
connect_config = {"server_url": server_url}
|
|
222
247
|
if workspace:
|
|
@@ -246,13 +271,14 @@ async def start_debugger(
|
|
|
246
271
|
"get_stack_trace": get_stack_trace,
|
|
247
272
|
"list_files": list_files,
|
|
248
273
|
"read_file": read_file,
|
|
274
|
+
"write_file": write_file,
|
|
249
275
|
}
|
|
250
276
|
|
|
251
277
|
svc_info = await server.register_service(service)
|
|
252
278
|
actual_id = svc_info.get("id", effective_id) if isinstance(svc_info, dict) else effective_id
|
|
253
279
|
ws = server.config.get("workspace", workspace) if hasattr(server.config, "get") else getattr(server.config, "workspace", workspace)
|
|
254
280
|
|
|
255
|
-
# Generate a token for remote access (24h expiry)
|
|
281
|
+
# Generate a token for remote access (24h expiry) only if requested.
|
|
256
282
|
session_token = ""
|
|
257
283
|
if require_token:
|
|
258
284
|
session_token = await server.generate_token({"expires_in": 86400})
|
|
@@ -266,7 +292,7 @@ async def start_debugger(
|
|
|
266
292
|
ws,
|
|
267
293
|
actual_id,
|
|
268
294
|
)
|
|
269
|
-
_print_session_info(server_url,
|
|
295
|
+
_print_session_info(server_url, service_url, session_token)
|
|
270
296
|
|
|
271
297
|
return DebugSession(
|
|
272
298
|
service_id=actual_id,
|
|
@@ -285,7 +311,7 @@ def start_debugger_sync(
|
|
|
285
311
|
service_id: str = "py-debugger",
|
|
286
312
|
service_name: str = "Python Debugger",
|
|
287
313
|
visibility: str = "",
|
|
288
|
-
require_token: bool =
|
|
314
|
+
require_token: bool = False,
|
|
289
315
|
) -> DebugSession:
|
|
290
316
|
"""Start the Hypha debugger (sync).
|
|
291
317
|
|
|
@@ -300,11 +326,9 @@ def start_debugger_sync(
|
|
|
300
326
|
"""
|
|
301
327
|
from hypha_rpc.sync import connect_to_server
|
|
302
328
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
effective_id = f"py-debugger-{secrets.token_hex(8)}"
|
|
307
|
-
effective_visibility = visibility or ("protected" if require_token else "unlisted")
|
|
329
|
+
effective_id, effective_visibility = _resolve_id_and_visibility(
|
|
330
|
+
service_id, visibility, require_token
|
|
331
|
+
)
|
|
308
332
|
|
|
309
333
|
server = connect_to_server({"server_url": server_url, **({"workspace": workspace} if workspace else {}), **({"token": token} if token else {})})
|
|
310
334
|
|
|
@@ -328,13 +352,14 @@ def start_debugger_sync(
|
|
|
328
352
|
"get_stack_trace": get_stack_trace,
|
|
329
353
|
"list_files": list_files,
|
|
330
354
|
"read_file": read_file,
|
|
355
|
+
"write_file": write_file,
|
|
331
356
|
}
|
|
332
357
|
|
|
333
358
|
svc_info = server.register_service(service)
|
|
334
359
|
actual_id = svc_info.get("id", effective_id) if isinstance(svc_info, dict) else effective_id
|
|
335
360
|
ws = server.config.get("workspace", workspace) if hasattr(server.config, "get") else getattr(server.config, "workspace", workspace)
|
|
336
361
|
|
|
337
|
-
# Generate a token for remote access (24h expiry)
|
|
362
|
+
# Generate a token for remote access (24h expiry) only if requested.
|
|
338
363
|
session_token = ""
|
|
339
364
|
if require_token:
|
|
340
365
|
session_token = server.generate_token({"expires_in": 86400})
|
|
@@ -348,7 +373,7 @@ def start_debugger_sync(
|
|
|
348
373
|
ws,
|
|
349
374
|
actual_id,
|
|
350
375
|
)
|
|
351
|
-
_print_session_info(server_url,
|
|
376
|
+
_print_session_info(server_url, service_url, session_token)
|
|
352
377
|
|
|
353
378
|
return DebugSession(
|
|
354
379
|
service_id=actual_id,
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
"""Arbitrary code execution service."""
|
|
1
|
+
"""Arbitrary code execution service with timeout support."""
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
4
|
import io
|
|
5
|
+
import signal
|
|
5
6
|
import traceback
|
|
7
|
+
import threading
|
|
8
|
+
|
|
9
|
+
# Default timeout for code execution (seconds). 0 = no timeout.
|
|
10
|
+
DEFAULT_TIMEOUT = 30
|
|
6
11
|
|
|
7
12
|
try:
|
|
8
13
|
from pydantic import Field
|
|
@@ -15,19 +20,28 @@ try:
|
|
|
15
20
|
default="__main__",
|
|
16
21
|
description='Namespace to execute in. Default: "__main__" (the main module namespace).',
|
|
17
22
|
),
|
|
23
|
+
timeout: int = Field(
|
|
24
|
+
default=DEFAULT_TIMEOUT,
|
|
25
|
+
description="Timeout in seconds. 0 for no timeout. Default: 30.",
|
|
26
|
+
),
|
|
18
27
|
) -> dict:
|
|
19
|
-
"""Execute Python code in the debugger process and return stdout, stderr, and the result
|
|
20
|
-
|
|
28
|
+
"""Execute Python code in the debugger process and return stdout, stderr, and the result.
|
|
29
|
+
|
|
30
|
+
Supports both expressions (returns the value) and statements.
|
|
31
|
+
Code runs in the target namespace so you can define functions, import modules, etc.
|
|
32
|
+
A timeout (default 30s) prevents infinite loops from hanging the debugger.
|
|
33
|
+
"""
|
|
34
|
+
return _execute_impl(code, namespace, timeout)
|
|
21
35
|
|
|
22
36
|
except ImportError:
|
|
23
37
|
|
|
24
|
-
def execute_code(code: str, namespace: str = "__main__") -> dict:
|
|
38
|
+
def execute_code(code: str, namespace: str = "__main__", timeout: int = DEFAULT_TIMEOUT) -> dict:
|
|
25
39
|
"""Execute Python code in the debugger process."""
|
|
26
|
-
return _execute_impl(code, namespace)
|
|
40
|
+
return _execute_impl(code, namespace, timeout)
|
|
27
41
|
|
|
28
42
|
|
|
29
|
-
def _execute_impl(code: str, namespace: str = "__main__") -> dict:
|
|
30
|
-
"""Implementation of code execution."""
|
|
43
|
+
def _execute_impl(code: str, namespace: str = "__main__", timeout: int = DEFAULT_TIMEOUT) -> dict:
|
|
44
|
+
"""Implementation of code execution with optional timeout."""
|
|
31
45
|
# Get the target namespace
|
|
32
46
|
if namespace == "__main__":
|
|
33
47
|
ns = vars(sys.modules.get("__main__", {}))
|
|
@@ -42,18 +56,30 @@ def _execute_impl(code: str, namespace: str = "__main__") -> dict:
|
|
|
42
56
|
|
|
43
57
|
result = None
|
|
44
58
|
error = None
|
|
59
|
+
timed_out = False
|
|
45
60
|
|
|
46
61
|
try:
|
|
47
62
|
sys.stdout = stdout_capture
|
|
48
63
|
sys.stderr = stderr_capture
|
|
49
64
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
if timeout > 0 and threading.current_thread() is threading.main_thread():
|
|
66
|
+
# Use SIGALRM for timeout on the main thread (Unix only)
|
|
67
|
+
def _timeout_handler(signum, frame):
|
|
68
|
+
raise TimeoutError(f"Code execution timed out after {timeout}s")
|
|
69
|
+
|
|
70
|
+
old_handler = signal.signal(signal.SIGALRM, _timeout_handler)
|
|
71
|
+
signal.alarm(timeout)
|
|
72
|
+
try:
|
|
73
|
+
result = _run_code(code, ns)
|
|
74
|
+
except TimeoutError as e:
|
|
75
|
+
error = str(e)
|
|
76
|
+
timed_out = True
|
|
77
|
+
finally:
|
|
78
|
+
signal.alarm(0)
|
|
79
|
+
signal.signal(signal.SIGALRM, old_handler)
|
|
80
|
+
else:
|
|
81
|
+
# No timeout or not on main thread — run directly
|
|
82
|
+
result = _run_code(code, ns)
|
|
57
83
|
except Exception:
|
|
58
84
|
error = traceback.format_exc()
|
|
59
85
|
finally:
|
|
@@ -74,10 +100,21 @@ def _execute_impl(code: str, namespace: str = "__main__") -> dict:
|
|
|
74
100
|
}
|
|
75
101
|
if error:
|
|
76
102
|
response["error"] = error
|
|
103
|
+
if timed_out:
|
|
104
|
+
response["timed_out"] = True
|
|
77
105
|
|
|
78
106
|
return response
|
|
79
107
|
|
|
80
108
|
|
|
109
|
+
def _run_code(code: str, ns: dict):
|
|
110
|
+
"""Try as expression first (to capture return value), fall back to exec."""
|
|
111
|
+
try:
|
|
112
|
+
return eval(code, ns)
|
|
113
|
+
except SyntaxError:
|
|
114
|
+
exec(code, ns)
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
81
118
|
def _safe_serialize(obj, depth=0, max_depth=3):
|
|
82
119
|
"""Safely serialize an object for RPC transport."""
|
|
83
120
|
if depth > max_depth:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""File system browsing service (sandboxed to CWD)."""
|
|
1
|
+
"""File system browsing and writing service (sandboxed to CWD)."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
|
|
@@ -27,13 +27,44 @@ try:
|
|
|
27
27
|
default=500,
|
|
28
28
|
description="Maximum number of lines to read. Default: 500.",
|
|
29
29
|
),
|
|
30
|
+
offset: int = Field(
|
|
31
|
+
default=0,
|
|
32
|
+
description="Number of lines to skip from the beginning. Default: 0.",
|
|
33
|
+
),
|
|
30
34
|
encoding: str = Field(
|
|
31
35
|
default="utf-8",
|
|
32
36
|
description="File encoding. Default: utf-8.",
|
|
33
37
|
),
|
|
34
38
|
) -> dict:
|
|
35
|
-
"""Read a file relative to the process CWD. Returns the content as a string.
|
|
36
|
-
|
|
39
|
+
"""Read a file relative to the process CWD. Returns the content as a string.
|
|
40
|
+
|
|
41
|
+
Use offset and max_lines to paginate through large files.
|
|
42
|
+
"""
|
|
43
|
+
return _read_file_impl(path, max_lines, offset, encoding)
|
|
44
|
+
|
|
45
|
+
@schema_function
|
|
46
|
+
def write_file(
|
|
47
|
+
path: str = Field(..., description="File path relative to CWD."),
|
|
48
|
+
content: str = Field(..., description="Content to write to the file."),
|
|
49
|
+
mode: str = Field(
|
|
50
|
+
default="w",
|
|
51
|
+
description='Write mode: "w" to overwrite, "a" to append. Default: "w".',
|
|
52
|
+
),
|
|
53
|
+
create_dirs: bool = Field(
|
|
54
|
+
default=True,
|
|
55
|
+
description="Create parent directories if they don't exist. Default: true.",
|
|
56
|
+
),
|
|
57
|
+
encoding: str = Field(
|
|
58
|
+
default="utf-8",
|
|
59
|
+
description="File encoding. Default: utf-8.",
|
|
60
|
+
),
|
|
61
|
+
) -> dict:
|
|
62
|
+
"""Write content to a file relative to the process CWD.
|
|
63
|
+
|
|
64
|
+
Creates parent directories automatically. Use mode="a" to append.
|
|
65
|
+
Sandboxed: cannot write outside the process CWD.
|
|
66
|
+
"""
|
|
67
|
+
return _write_file_impl(path, content, mode, create_dirs, encoding)
|
|
37
68
|
|
|
38
69
|
except ImportError:
|
|
39
70
|
|
|
@@ -42,10 +73,17 @@ except ImportError:
|
|
|
42
73
|
return _list_files_impl(path, pattern)
|
|
43
74
|
|
|
44
75
|
def read_file(
|
|
45
|
-
path: str = "", max_lines: int = 500, encoding: str = "utf-8"
|
|
76
|
+
path: str = "", max_lines: int = 500, offset: int = 0, encoding: str = "utf-8"
|
|
46
77
|
) -> dict:
|
|
47
78
|
"""Read a file relative to CWD."""
|
|
48
|
-
return _read_file_impl(path, max_lines, encoding)
|
|
79
|
+
return _read_file_impl(path, max_lines, offset, encoding)
|
|
80
|
+
|
|
81
|
+
def write_file(
|
|
82
|
+
path: str = "", content: str = "", mode: str = "w",
|
|
83
|
+
create_dirs: bool = True, encoding: str = "utf-8"
|
|
84
|
+
) -> dict:
|
|
85
|
+
"""Write content to a file relative to CWD."""
|
|
86
|
+
return _write_file_impl(path, content, mode, create_dirs, encoding)
|
|
49
87
|
|
|
50
88
|
|
|
51
89
|
def _resolve_safe(path: str) -> str:
|
|
@@ -105,7 +143,7 @@ def _entry_info(name: str, full_path: str) -> dict:
|
|
|
105
143
|
return info
|
|
106
144
|
|
|
107
145
|
|
|
108
|
-
def _read_file_impl(path: str, max_lines: int, encoding: str) -> dict:
|
|
146
|
+
def _read_file_impl(path: str, max_lines: int, offset: int, encoding: str) -> dict:
|
|
109
147
|
try:
|
|
110
148
|
resolved = _resolve_safe(path)
|
|
111
149
|
except PermissionError as e:
|
|
@@ -118,7 +156,9 @@ def _read_file_impl(path: str, max_lines: int, encoding: str) -> dict:
|
|
|
118
156
|
with open(resolved, "r", encoding=encoding, errors="replace") as f:
|
|
119
157
|
lines = []
|
|
120
158
|
for i, line in enumerate(f):
|
|
121
|
-
if i
|
|
159
|
+
if i < offset:
|
|
160
|
+
continue
|
|
161
|
+
if len(lines) >= max_lines:
|
|
122
162
|
break
|
|
123
163
|
lines.append(line)
|
|
124
164
|
content = "".join(lines)
|
|
@@ -126,7 +166,35 @@ def _read_file_impl(path: str, max_lines: int, encoding: str) -> dict:
|
|
|
126
166
|
"path": os.path.relpath(resolved, os.getcwd()),
|
|
127
167
|
"content": content,
|
|
128
168
|
"lines_read": len(lines),
|
|
169
|
+
"offset": offset,
|
|
129
170
|
"truncated": len(lines) >= max_lines,
|
|
130
171
|
}
|
|
131
172
|
except Exception as e:
|
|
132
173
|
return {"error": f"Failed to read file: {e}"}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _write_file_impl(
|
|
177
|
+
path: str, content: str, mode: str, create_dirs: bool, encoding: str
|
|
178
|
+
) -> dict:
|
|
179
|
+
if mode not in ("w", "a"):
|
|
180
|
+
return {"error": f'Invalid mode: {mode}. Use "w" or "a".'}
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
resolved = _resolve_safe(path)
|
|
184
|
+
except PermissionError as e:
|
|
185
|
+
return {"error": str(e)}
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
if create_dirs:
|
|
189
|
+
os.makedirs(os.path.dirname(resolved), exist_ok=True)
|
|
190
|
+
|
|
191
|
+
with open(resolved, mode, encoding=encoding) as f:
|
|
192
|
+
f.write(content)
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
"path": os.path.relpath(resolved, os.getcwd()),
|
|
196
|
+
"bytes_written": len(content.encode(encoding)),
|
|
197
|
+
"mode": mode,
|
|
198
|
+
}
|
|
199
|
+
except Exception as e:
|
|
200
|
+
return {"error": f"Failed to write file: {e}"}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Basic tests for service functions (no server required)."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
|
|
7
|
+
from hypha_debugger.services.info import get_process_info, get_installed_packages
|
|
8
|
+
from hypha_debugger.services.execute import execute_code
|
|
9
|
+
from hypha_debugger.services.inspect_vars import get_variable, list_variables, get_stack_trace
|
|
10
|
+
from hypha_debugger.services.filesystem import list_files, read_file, write_file
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_get_process_info():
|
|
14
|
+
info = get_process_info()
|
|
15
|
+
assert info["pid"] == os.getpid()
|
|
16
|
+
assert info["python_version"] == sys.version
|
|
17
|
+
assert "cwd" in info
|
|
18
|
+
assert "hostname" in info
|
|
19
|
+
assert "platform" in info
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_get_installed_packages():
|
|
23
|
+
pkgs = get_installed_packages()
|
|
24
|
+
assert isinstance(pkgs, list)
|
|
25
|
+
assert len(pkgs) > 0
|
|
26
|
+
# Should find hypha-debugger itself
|
|
27
|
+
names = [p["name"].lower() for p in pkgs]
|
|
28
|
+
assert "hypha-debugger" in names
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_get_installed_packages_filter():
|
|
32
|
+
pkgs = get_installed_packages("hypha")
|
|
33
|
+
names = [p["name"].lower() for p in pkgs]
|
|
34
|
+
assert all("hypha" in n for n in names)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# --- execute_code ---
|
|
38
|
+
|
|
39
|
+
def test_execute_code_expression():
|
|
40
|
+
result = execute_code("1 + 2")
|
|
41
|
+
assert result["result"] == 3
|
|
42
|
+
assert result["result_type"] == "int"
|
|
43
|
+
assert result.get("error") is None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_execute_code_statement():
|
|
47
|
+
result = execute_code("x = 42\nprint(x)")
|
|
48
|
+
assert "42" in result["stdout"]
|
|
49
|
+
assert result.get("error") is None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_execute_code_error():
|
|
53
|
+
result = execute_code("1 / 0")
|
|
54
|
+
assert "error" in result
|
|
55
|
+
assert "ZeroDivisionError" in result["error"]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_execute_code_import():
|
|
59
|
+
# Multi-statement uses exec (no return value), so test via stdout
|
|
60
|
+
result = execute_code("import json; print(json.dumps({'a': 1}))")
|
|
61
|
+
assert '{"a": 1}' in result["stdout"]
|
|
62
|
+
assert result.get("error") is None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_execute_code_multiline():
|
|
66
|
+
code = """
|
|
67
|
+
def greet(name):
|
|
68
|
+
return f"Hello, {name}!"
|
|
69
|
+
greet("World")
|
|
70
|
+
"""
|
|
71
|
+
result = execute_code(code)
|
|
72
|
+
assert result.get("error") is None
|
|
73
|
+
# eval/exec may or may not capture the last expression depending on implementation
|
|
74
|
+
# but at least no error
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_execute_code_timeout():
|
|
78
|
+
"""Test that timeout parameter works (execution should not hang)."""
|
|
79
|
+
result = execute_code("'done'", timeout=5)
|
|
80
|
+
assert result["result"] == "done"
|
|
81
|
+
assert result.get("error") is None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_execute_code_stdout_capture():
|
|
85
|
+
result = execute_code("print('hello'); print('world')")
|
|
86
|
+
assert "hello" in result["stdout"]
|
|
87
|
+
assert "world" in result["stdout"]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# --- inspect_vars ---
|
|
91
|
+
|
|
92
|
+
def test_get_variable():
|
|
93
|
+
result = get_variable("sys", namespace="hypha_debugger.services.inspect_vars")
|
|
94
|
+
assert result["name"] == "sys"
|
|
95
|
+
assert result["type"] == "module"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_get_variable_not_found():
|
|
99
|
+
result = get_variable("nonexistent_variable_xyz")
|
|
100
|
+
assert "error" in result
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_list_variables():
|
|
104
|
+
result = list_variables(namespace="hypha_debugger.services.info")
|
|
105
|
+
names = [v["name"] for v in result]
|
|
106
|
+
assert "get_process_info" in names
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_get_stack_trace():
|
|
110
|
+
traces = get_stack_trace()
|
|
111
|
+
assert isinstance(traces, list)
|
|
112
|
+
assert len(traces) > 0
|
|
113
|
+
assert "thread_id" in traces[0]
|
|
114
|
+
assert "thread_name" in traces[0]
|
|
115
|
+
assert "stack" in traces[0]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# --- filesystem ---
|
|
119
|
+
|
|
120
|
+
def test_list_files():
|
|
121
|
+
result = list_files(".")
|
|
122
|
+
assert "entries" in result
|
|
123
|
+
assert "total" in result
|
|
124
|
+
names = [e["name"] for e in result["entries"]]
|
|
125
|
+
assert any("hypha_debugger" in n for n in names) or len(names) > 0
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_list_files_pattern():
|
|
129
|
+
result = list_files(".", pattern="*.toml")
|
|
130
|
+
names = [e["name"] for e in result["entries"]]
|
|
131
|
+
assert "pyproject.toml" in names
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_read_file():
|
|
135
|
+
result = read_file("pyproject.toml")
|
|
136
|
+
if "error" not in result:
|
|
137
|
+
assert "content" in result
|
|
138
|
+
assert "hypha-debugger" in result["content"]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_read_file_with_offset():
|
|
142
|
+
result = read_file("pyproject.toml", offset=1, max_lines=2)
|
|
143
|
+
assert result.get("error") is None
|
|
144
|
+
assert result["offset"] == 1
|
|
145
|
+
assert result["lines_read"] <= 2
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_read_file_sandbox():
|
|
149
|
+
result = read_file("../../etc/passwd")
|
|
150
|
+
assert "error" in result
|
|
151
|
+
assert "Access denied" in result["error"] or "Not a file" in result["error"]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_write_file():
|
|
155
|
+
old_cwd = os.getcwd()
|
|
156
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
157
|
+
os.chdir(tmpdir)
|
|
158
|
+
try:
|
|
159
|
+
result = write_file("test_output.txt", "hello world")
|
|
160
|
+
assert result.get("error") is None
|
|
161
|
+
assert result["bytes_written"] == 11
|
|
162
|
+
assert result["mode"] == "w"
|
|
163
|
+
|
|
164
|
+
# Verify written content
|
|
165
|
+
read_result = read_file("test_output.txt")
|
|
166
|
+
assert read_result["content"] == "hello world"
|
|
167
|
+
finally:
|
|
168
|
+
os.chdir(old_cwd)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_write_file_append():
|
|
172
|
+
old_cwd = os.getcwd()
|
|
173
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
174
|
+
os.chdir(tmpdir)
|
|
175
|
+
try:
|
|
176
|
+
write_file("append_test.txt", "first\n")
|
|
177
|
+
write_file("append_test.txt", "second\n", mode="a")
|
|
178
|
+
read_result = read_file("append_test.txt")
|
|
179
|
+
assert "first" in read_result["content"]
|
|
180
|
+
assert "second" in read_result["content"]
|
|
181
|
+
finally:
|
|
182
|
+
os.chdir(old_cwd)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_write_file_create_dirs():
|
|
186
|
+
old_cwd = os.getcwd()
|
|
187
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
188
|
+
os.chdir(tmpdir)
|
|
189
|
+
try:
|
|
190
|
+
result = write_file("sub/dir/test.txt", "nested content")
|
|
191
|
+
assert result.get("error") is None
|
|
192
|
+
assert os.path.isfile(os.path.join(tmpdir, "sub", "dir", "test.txt"))
|
|
193
|
+
finally:
|
|
194
|
+
os.chdir(old_cwd)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_write_file_sandbox():
|
|
198
|
+
result = write_file("../../etc/evil.txt", "pwned")
|
|
199
|
+
assert "error" in result
|
|
200
|
+
assert "Access denied" in result["error"]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_write_file_invalid_mode():
|
|
204
|
+
result = write_file("test.txt", "content", mode="x")
|
|
205
|
+
assert "error" in result
|
|
206
|
+
assert "Invalid mode" in result["error"]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# --- instruction block ---
|
|
210
|
+
|
|
211
|
+
def test_instruction_block_no_token():
|
|
212
|
+
from hypha_debugger.debugger import _build_instruction_block
|
|
213
|
+
block = _build_instruction_block("https://example.com/ws/services/py-debugger-abc")
|
|
214
|
+
assert "SERVICE_URL=" in block
|
|
215
|
+
assert "TOKEN=" not in block
|
|
216
|
+
assert "Authorization" not in block
|
|
217
|
+
assert "execute_code" in block
|
|
218
|
+
assert "write_file" in block
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_instruction_block_with_token():
|
|
222
|
+
from hypha_debugger.debugger import _build_instruction_block
|
|
223
|
+
block = _build_instruction_block("https://example.com/ws/services/py-debugger", "mytoken123")
|
|
224
|
+
assert 'TOKEN="mytoken123"' in block
|
|
225
|
+
assert "Authorization" in block
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_debug_session_print_instructions(capsys):
|
|
229
|
+
from hypha_debugger.debugger import DebugSession
|
|
230
|
+
session = DebugSession(
|
|
231
|
+
service_id="test", workspace="test", server=None,
|
|
232
|
+
service_url="https://example.com/ws/services/py-debugger-abc",
|
|
233
|
+
)
|
|
234
|
+
result = session.print_instructions()
|
|
235
|
+
captured = capsys.readouterr()
|
|
236
|
+
assert "WARNING" in captured.out
|
|
237
|
+
assert "trusted" in captured.out
|
|
238
|
+
assert "SERVICE_URL=" in captured.out
|
|
239
|
+
assert isinstance(result, str)
|
|
240
|
+
assert "SERVICE_URL=" in result
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
"""Basic tests for service functions (no server required)."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
|
|
6
|
-
from hypha_debugger.services.info import get_process_info
|
|
7
|
-
from hypha_debugger.services.execute import execute_code
|
|
8
|
-
from hypha_debugger.services.inspect_vars import get_variable, list_variables
|
|
9
|
-
from hypha_debugger.services.filesystem import list_files, read_file
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def test_get_process_info():
|
|
13
|
-
info = get_process_info()
|
|
14
|
-
assert info["pid"] == os.getpid()
|
|
15
|
-
assert info["python_version"] == sys.version
|
|
16
|
-
assert "cwd" in info
|
|
17
|
-
assert "hostname" in info
|
|
18
|
-
assert "platform" in info
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def test_execute_code_expression():
|
|
22
|
-
result = execute_code("1 + 2")
|
|
23
|
-
assert result["result"] == 3
|
|
24
|
-
assert result["result_type"] == "int"
|
|
25
|
-
assert result.get("error") is None
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def test_execute_code_statement():
|
|
29
|
-
result = execute_code("x = 42\nprint(x)")
|
|
30
|
-
assert "42" in result["stdout"]
|
|
31
|
-
assert result.get("error") is None
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def test_execute_code_error():
|
|
35
|
-
result = execute_code("1 / 0")
|
|
36
|
-
assert "error" in result
|
|
37
|
-
assert "ZeroDivisionError" in result["error"]
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def test_get_variable():
|
|
41
|
-
result = get_variable("sys", namespace="hypha_debugger.services.inspect_vars")
|
|
42
|
-
assert result["name"] == "sys"
|
|
43
|
-
assert result["type"] == "module"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_get_variable_not_found():
|
|
47
|
-
result = get_variable("nonexistent_variable_xyz")
|
|
48
|
-
assert "error" in result
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def test_list_variables():
|
|
52
|
-
result = list_variables(namespace="hypha_debugger.services.info")
|
|
53
|
-
names = [v["name"] for v in result]
|
|
54
|
-
assert "get_process_info" in names
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def test_list_files():
|
|
58
|
-
result = list_files(".")
|
|
59
|
-
assert "entries" in result
|
|
60
|
-
assert "total" in result
|
|
61
|
-
# Should find the pyproject.toml
|
|
62
|
-
names = [e["name"] for e in result["entries"]]
|
|
63
|
-
assert any("hypha_debugger" in n for n in names) or len(names) > 0
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def test_read_file():
|
|
67
|
-
# Read the pyproject.toml
|
|
68
|
-
result = read_file("pyproject.toml")
|
|
69
|
-
if "error" not in result:
|
|
70
|
-
assert "content" in result
|
|
71
|
-
assert "hypha-debugger" in result["content"]
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def test_read_file_sandbox():
|
|
75
|
-
result = read_file("../../etc/passwd")
|
|
76
|
-
assert "error" in result
|
|
77
|
-
assert "Access denied" in result["error"] or "Not a file" in result["error"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|