gitarsenal-cli 1.6.15 → 1.7.2
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.
- package/package.json +1 -1
- package/python/requirements.txt +2 -1
- package/python/test_modalSandboxScript.py +490 -106
- package/test_modalSandboxScript.py +490 -106
@@ -10,6 +10,9 @@ import requests
|
|
10
10
|
import secrets
|
11
11
|
import string
|
12
12
|
import argparse
|
13
|
+
import threading
|
14
|
+
import uuid
|
15
|
+
import signal
|
13
16
|
from pathlib import Path
|
14
17
|
|
15
18
|
# Parse command-line arguments
|
@@ -33,6 +36,382 @@ if args.proxy_api_key:
|
|
33
36
|
os.environ["MODAL_PROXY_API_KEY"] = args.proxy_api_key
|
34
37
|
# print(f"✅ Set MODAL_PROXY_API_KEY from command line")
|
35
38
|
|
39
|
+
class PersistentShell:
|
40
|
+
"""A persistent bash shell using subprocess.Popen for executing commands with state persistence."""
|
41
|
+
|
42
|
+
def __init__(self, working_dir="/root", timeout=60):
|
43
|
+
self.working_dir = working_dir
|
44
|
+
self.timeout = timeout
|
45
|
+
self.process = None
|
46
|
+
self.stdout_lines = [] # Use list instead of queue
|
47
|
+
self.stderr_lines = [] # Use list instead of queue
|
48
|
+
self.stdout_lock = threading.Lock()
|
49
|
+
self.stderr_lock = threading.Lock()
|
50
|
+
self.stdout_thread = None
|
51
|
+
self.stderr_thread = None
|
52
|
+
self.command_counter = 0
|
53
|
+
self.is_running = False
|
54
|
+
self.virtual_env_path = None # Track activated virtual environment
|
55
|
+
|
56
|
+
def start(self):
|
57
|
+
"""Start the persistent bash shell."""
|
58
|
+
if self.is_running:
|
59
|
+
return
|
60
|
+
|
61
|
+
print(f"🐚 Starting persistent bash shell in {self.working_dir}")
|
62
|
+
|
63
|
+
# Start bash with unbuffered output
|
64
|
+
self.process = subprocess.Popen(
|
65
|
+
['bash', '-i'], # Interactive bash
|
66
|
+
stdin=subprocess.PIPE,
|
67
|
+
stdout=subprocess.PIPE,
|
68
|
+
stderr=subprocess.PIPE,
|
69
|
+
text=True,
|
70
|
+
bufsize=0, # Unbuffered
|
71
|
+
cwd=self.working_dir,
|
72
|
+
preexec_fn=os.setsid # Create new process group
|
73
|
+
)
|
74
|
+
|
75
|
+
# Start threads to read stdout and stderr
|
76
|
+
self.stdout_thread = threading.Thread(target=self._read_stdout, daemon=True)
|
77
|
+
self.stderr_thread = threading.Thread(target=self._read_stderr, daemon=True)
|
78
|
+
|
79
|
+
self.stdout_thread.start()
|
80
|
+
self.stderr_thread.start()
|
81
|
+
|
82
|
+
self.is_running = True
|
83
|
+
|
84
|
+
# Initial setup commands
|
85
|
+
self._send_command_raw("set +h") # Disable hash table for commands
|
86
|
+
self._send_command_raw("export PS1='$ '") # Simpler prompt
|
87
|
+
self._send_command_raw("cd " + self.working_dir) # Change to working directory
|
88
|
+
time.sleep(0.5) # Let initial commands settle
|
89
|
+
|
90
|
+
print("✅ Persistent shell started successfully")
|
91
|
+
|
92
|
+
def _read_stdout(self):
|
93
|
+
"""Read stdout in a separate thread."""
|
94
|
+
while self.process and self.process.poll() is None:
|
95
|
+
try:
|
96
|
+
line = self.process.stdout.readline()
|
97
|
+
if line:
|
98
|
+
with self.stdout_lock:
|
99
|
+
self.stdout_lines.append(line.rstrip('\n'))
|
100
|
+
else:
|
101
|
+
time.sleep(0.01)
|
102
|
+
except Exception as e:
|
103
|
+
print(f"Error reading stdout: {e}")
|
104
|
+
break
|
105
|
+
|
106
|
+
def _read_stderr(self):
|
107
|
+
"""Read stderr in a separate thread."""
|
108
|
+
while self.process and self.process.poll() is None:
|
109
|
+
try:
|
110
|
+
line = self.process.stderr.readline()
|
111
|
+
if line:
|
112
|
+
with self.stderr_lock:
|
113
|
+
self.stderr_lines.append(line.rstrip('\n'))
|
114
|
+
else:
|
115
|
+
time.sleep(0.01)
|
116
|
+
except Exception as e:
|
117
|
+
print(f"Error reading stderr: {e}")
|
118
|
+
break
|
119
|
+
|
120
|
+
def _send_command_raw(self, command):
|
121
|
+
"""Send a raw command to the shell without waiting for completion."""
|
122
|
+
if not self.is_running or not self.process:
|
123
|
+
raise RuntimeError("Shell is not running")
|
124
|
+
|
125
|
+
try:
|
126
|
+
self.process.stdin.write(command + '\n')
|
127
|
+
self.process.stdin.flush()
|
128
|
+
except Exception as e:
|
129
|
+
print(f"Error sending command: {e}")
|
130
|
+
raise
|
131
|
+
|
132
|
+
def _preprocess_command(self, command):
|
133
|
+
"""Preprocess commands to handle special cases like virtual environment activation."""
|
134
|
+
# Handle virtual environment creation and activation
|
135
|
+
if "uv venv" in command and "&&" in command and "source" in command:
|
136
|
+
# Split the compound command into separate parts
|
137
|
+
parts = [part.strip() for part in command.split("&&")]
|
138
|
+
return parts
|
139
|
+
elif command.strip().startswith("source ") and "/bin/activate" in command:
|
140
|
+
# Handle standalone source command
|
141
|
+
venv_path = command.replace("source ", "").replace("/bin/activate", "").strip()
|
142
|
+
self.virtual_env_path = venv_path
|
143
|
+
return [command]
|
144
|
+
elif "source" in command and "activate" in command:
|
145
|
+
# Handle any other source activation pattern
|
146
|
+
return [command]
|
147
|
+
elif "uv pip install" in command and self.is_in_venv():
|
148
|
+
# If we're in a virtual environment, ensure we use the right pip
|
149
|
+
return [command]
|
150
|
+
else:
|
151
|
+
return [command]
|
152
|
+
|
153
|
+
def execute(self, command, timeout=None):
|
154
|
+
"""Execute a command and return (success, stdout, stderr)."""
|
155
|
+
if not self.is_running:
|
156
|
+
self.start()
|
157
|
+
|
158
|
+
if timeout is None:
|
159
|
+
timeout = self.timeout
|
160
|
+
|
161
|
+
# Preprocess the command to handle special cases
|
162
|
+
command_parts = self._preprocess_command(command)
|
163
|
+
|
164
|
+
# If we have multiple parts, execute them sequentially
|
165
|
+
if len(command_parts) > 1:
|
166
|
+
print(f"🔧 Executing compound command in {len(command_parts)} parts")
|
167
|
+
all_stdout = []
|
168
|
+
all_stderr = []
|
169
|
+
|
170
|
+
for i, part in enumerate(command_parts):
|
171
|
+
print(f" Part {i+1}/{len(command_parts)}: {part}")
|
172
|
+
success, stdout, stderr = self._execute_single(part, timeout)
|
173
|
+
|
174
|
+
if stdout:
|
175
|
+
all_stdout.append(stdout)
|
176
|
+
if stderr:
|
177
|
+
all_stderr.append(stderr)
|
178
|
+
|
179
|
+
if not success:
|
180
|
+
# If any part fails, return the failure
|
181
|
+
return False, '\n'.join(all_stdout), '\n'.join(all_stderr)
|
182
|
+
|
183
|
+
# Small delay between parts to let environment changes take effect
|
184
|
+
time.sleep(0.1)
|
185
|
+
|
186
|
+
return True, '\n'.join(all_stdout), '\n'.join(all_stderr)
|
187
|
+
else:
|
188
|
+
return self._execute_single(command_parts[0], timeout)
|
189
|
+
|
190
|
+
def _execute_single(self, command, timeout):
|
191
|
+
"""Execute a single command and return (success, stdout, stderr)."""
|
192
|
+
self.command_counter += 1
|
193
|
+
marker = f"CMD_DONE_{self.command_counter}_{uuid.uuid4().hex[:8]}"
|
194
|
+
|
195
|
+
print(f"🔧 Executing: {command}")
|
196
|
+
|
197
|
+
# Clear any existing output
|
198
|
+
self._clear_lines()
|
199
|
+
|
200
|
+
# Wait for shell to be ready (prompt should be visible)
|
201
|
+
if not self.wait_for_prompt(timeout=2):
|
202
|
+
print("⚠️ Shell not ready, waiting...")
|
203
|
+
time.sleep(0.5)
|
204
|
+
|
205
|
+
# For source commands, we need special handling
|
206
|
+
if command.strip().startswith("source "):
|
207
|
+
# Send the source command in a way that preserves the environment
|
208
|
+
try:
|
209
|
+
# Extract the virtual environment path
|
210
|
+
venv_path = command.replace("source ", "").replace("/bin/activate", "").strip()
|
211
|
+
|
212
|
+
# Use a more robust approach that actually activates the environment
|
213
|
+
activation_script = f"""
|
214
|
+
if [ -f "{venv_path}/bin/activate" ]; then
|
215
|
+
source "{venv_path}/bin/activate"
|
216
|
+
echo "VIRTUAL_ENV=$VIRTUAL_ENV"
|
217
|
+
echo "PATH=$PATH"
|
218
|
+
echo 'SOURCE_SUCCESS'
|
219
|
+
else
|
220
|
+
echo 'SOURCE_FAILED - activation script not found'
|
221
|
+
fi
|
222
|
+
"""
|
223
|
+
|
224
|
+
self._send_command_raw(activation_script)
|
225
|
+
time.sleep(0.3) # Give more time for environment changes
|
226
|
+
self._send_command_raw(f'echo "EXIT_CODE:$?"')
|
227
|
+
self._send_command_raw(f'echo "{marker}"')
|
228
|
+
except Exception as e:
|
229
|
+
return False, "", f"Failed to send source command: {e}"
|
230
|
+
else:
|
231
|
+
# Send the command followed by markers
|
232
|
+
try:
|
233
|
+
self._send_command_raw(command)
|
234
|
+
# Wait a moment for the command to start
|
235
|
+
time.sleep(0.1)
|
236
|
+
self._send_command_raw(f'echo "EXIT_CODE:$?"')
|
237
|
+
self._send_command_raw(f'echo "{marker}"')
|
238
|
+
except Exception as e:
|
239
|
+
return False, "", f"Failed to send command: {e}"
|
240
|
+
|
241
|
+
# Collect output until we see the marker
|
242
|
+
command_stdout = []
|
243
|
+
command_stderr = []
|
244
|
+
start_time = time.time()
|
245
|
+
found_marker = False
|
246
|
+
exit_code = None
|
247
|
+
last_stdout_index = 0
|
248
|
+
last_stderr_index = 0
|
249
|
+
source_success = None
|
250
|
+
|
251
|
+
while time.time() - start_time < timeout:
|
252
|
+
# Check for new stdout lines
|
253
|
+
with self.stdout_lock:
|
254
|
+
current_stdout = self.stdout_lines[last_stdout_index:]
|
255
|
+
last_stdout_index = len(self.stdout_lines)
|
256
|
+
|
257
|
+
for line in current_stdout:
|
258
|
+
if line == marker:
|
259
|
+
found_marker = True
|
260
|
+
break
|
261
|
+
elif line.startswith("EXIT_CODE:"):
|
262
|
+
try:
|
263
|
+
exit_code = int(line.split(":", 1)[1])
|
264
|
+
except (ValueError, IndexError):
|
265
|
+
exit_code = 1
|
266
|
+
elif line == "SOURCE_SUCCESS":
|
267
|
+
source_success = True
|
268
|
+
elif line.startswith("SOURCE_FAILED"):
|
269
|
+
source_success = False
|
270
|
+
command_stderr.append(line)
|
271
|
+
elif line.startswith("VIRTUAL_ENV="):
|
272
|
+
# Extract and store the virtual environment path
|
273
|
+
venv_path = line.split("=", 1)[1]
|
274
|
+
self.virtual_env_path = venv_path
|
275
|
+
command_stdout.append(line)
|
276
|
+
elif line.startswith("PATH="):
|
277
|
+
# Store the updated PATH
|
278
|
+
command_stdout.append(line)
|
279
|
+
elif line.strip() and not line.startswith("$"): # Skip empty lines and prompt lines
|
280
|
+
command_stdout.append(line)
|
281
|
+
|
282
|
+
if found_marker:
|
283
|
+
break
|
284
|
+
|
285
|
+
# Check for new stderr lines
|
286
|
+
with self.stderr_lock:
|
287
|
+
current_stderr = self.stderr_lines[last_stderr_index:]
|
288
|
+
last_stderr_index = len(self.stderr_lines)
|
289
|
+
|
290
|
+
for line in current_stderr:
|
291
|
+
if line.strip(): # Skip empty lines
|
292
|
+
command_stderr.append(line)
|
293
|
+
|
294
|
+
time.sleep(0.1)
|
295
|
+
|
296
|
+
if not found_marker:
|
297
|
+
print(f"⚠️ Command timed out after {timeout} seconds")
|
298
|
+
return False, '\n'.join(command_stdout), f"Command timed out after {timeout} seconds"
|
299
|
+
|
300
|
+
stdout_text = '\n'.join(command_stdout)
|
301
|
+
stderr_text = '\n'.join(command_stderr)
|
302
|
+
|
303
|
+
# Determine success based on multiple factors
|
304
|
+
if source_success is not None:
|
305
|
+
success = source_success
|
306
|
+
else:
|
307
|
+
success = exit_code == 0 if exit_code is not None else len(command_stderr) == 0
|
308
|
+
|
309
|
+
if success:
|
310
|
+
if stdout_text:
|
311
|
+
print(f"✅ Output: {stdout_text}")
|
312
|
+
# Track virtual environment activation
|
313
|
+
if command.strip().startswith("source ") and "/bin/activate" in command:
|
314
|
+
venv_path = command.replace("source ", "").replace("/bin/activate", "").strip()
|
315
|
+
self.virtual_env_path = venv_path
|
316
|
+
print(f"✅ Virtual environment activated: {venv_path}")
|
317
|
+
else:
|
318
|
+
print(f"❌ Command failed with exit code: {exit_code}")
|
319
|
+
if stderr_text:
|
320
|
+
print(f"❌ Error: {stderr_text}")
|
321
|
+
|
322
|
+
# Wait a moment for the shell to be ready for the next command
|
323
|
+
time.sleep(0.2)
|
324
|
+
|
325
|
+
return success, stdout_text, stderr_text
|
326
|
+
|
327
|
+
def _clear_lines(self):
|
328
|
+
"""Clear both output line lists."""
|
329
|
+
with self.stdout_lock:
|
330
|
+
self.stdout_lines.clear()
|
331
|
+
with self.stderr_lock:
|
332
|
+
self.stderr_lines.clear()
|
333
|
+
|
334
|
+
def get_cwd(self):
|
335
|
+
"""Get current working directory."""
|
336
|
+
success, output, _ = self._execute_single("pwd", 10)
|
337
|
+
if success:
|
338
|
+
return output.strip()
|
339
|
+
return self.working_dir
|
340
|
+
|
341
|
+
def get_virtual_env(self):
|
342
|
+
"""Get the currently activated virtual environment path."""
|
343
|
+
return self.virtual_env_path
|
344
|
+
|
345
|
+
def is_in_venv(self):
|
346
|
+
"""Check if we're currently in a virtual environment."""
|
347
|
+
return self.virtual_env_path is not None and self.virtual_env_path != ""
|
348
|
+
|
349
|
+
def get_venv_name(self):
|
350
|
+
"""Get the name of the current virtual environment if active."""
|
351
|
+
if self.is_in_venv():
|
352
|
+
return os.path.basename(self.virtual_env_path)
|
353
|
+
return None
|
354
|
+
|
355
|
+
def exec(self, *args, **kwargs):
|
356
|
+
"""Compatibility method to make PersistentShell work with call_openai_for_debug."""
|
357
|
+
# Convert exec call to execute method
|
358
|
+
if len(args) >= 2 and args[0] == "bash" and args[1] == "-c":
|
359
|
+
command = args[2]
|
360
|
+
success, stdout, stderr = self.execute(command)
|
361
|
+
|
362
|
+
# Create a mock result object that mimics the expected interface
|
363
|
+
class MockResult:
|
364
|
+
def __init__(self, stdout, stderr, returncode):
|
365
|
+
self.stdout = [stdout] if stdout else []
|
366
|
+
self.stderr = [stderr] if stderr else []
|
367
|
+
self.returncode = 0 if returncode else 1
|
368
|
+
|
369
|
+
def wait(self):
|
370
|
+
pass
|
371
|
+
|
372
|
+
return MockResult(stdout, stderr, success)
|
373
|
+
else:
|
374
|
+
raise NotImplementedError("exec method only supports bash -c commands")
|
375
|
+
|
376
|
+
def wait_for_prompt(self, timeout=5):
|
377
|
+
"""Wait for the shell prompt to appear, indicating readiness for next command."""
|
378
|
+
start_time = time.time()
|
379
|
+
while time.time() - start_time < timeout:
|
380
|
+
with self.stdout_lock:
|
381
|
+
if self.stdout_lines and self.stdout_lines[-1].strip().endswith('$'):
|
382
|
+
return True
|
383
|
+
time.sleep(0.1)
|
384
|
+
return False
|
385
|
+
|
386
|
+
def cleanup(self):
|
387
|
+
"""Clean up the shell process."""
|
388
|
+
print("🧹 Cleaning up persistent shell...")
|
389
|
+
self.is_running = False
|
390
|
+
|
391
|
+
if self.process:
|
392
|
+
try:
|
393
|
+
# Send exit command
|
394
|
+
self._send_command_raw("exit")
|
395
|
+
|
396
|
+
# Wait for process to terminate
|
397
|
+
try:
|
398
|
+
self.process.wait(timeout=5)
|
399
|
+
except subprocess.TimeoutExpired:
|
400
|
+
# Force kill if it doesn't exit gracefully
|
401
|
+
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
|
402
|
+
try:
|
403
|
+
self.process.wait(timeout=2)
|
404
|
+
except subprocess.TimeoutExpired:
|
405
|
+
os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
|
406
|
+
|
407
|
+
except Exception as e:
|
408
|
+
print(f"Error during cleanup: {e}")
|
409
|
+
finally:
|
410
|
+
self.process = None
|
411
|
+
|
412
|
+
print("✅ Shell cleanup completed")
|
413
|
+
|
414
|
+
|
36
415
|
# First, try to fetch tokens from the proxy server
|
37
416
|
try:
|
38
417
|
# Import the fetch_modal_tokens module
|
@@ -327,6 +706,37 @@ def handle_huggingface_login(sandbox, current_dir):
|
|
327
706
|
return exit_code == 0, stdout_buffer, stderr_buffer
|
328
707
|
|
329
708
|
|
709
|
+
def get_stored_credentials():
|
710
|
+
"""Load stored credentials from ~/.gitarsenal/credentials.json"""
|
711
|
+
import json
|
712
|
+
from pathlib import Path
|
713
|
+
|
714
|
+
try:
|
715
|
+
credentials_file = Path.home() / ".gitarsenal" / "credentials.json"
|
716
|
+
if credentials_file.exists():
|
717
|
+
with open(credentials_file, 'r') as f:
|
718
|
+
credentials = json.load(f)
|
719
|
+
return credentials
|
720
|
+
else:
|
721
|
+
return {}
|
722
|
+
except Exception as e:
|
723
|
+
print(f"⚠️ Error loading stored credentials: {e}")
|
724
|
+
return {}
|
725
|
+
|
726
|
+
def generate_auth_context(stored_credentials):
|
727
|
+
"""Generate simple authentication context for the OpenAI prompt"""
|
728
|
+
if not stored_credentials:
|
729
|
+
return "No stored credentials available."
|
730
|
+
|
731
|
+
auth_context = "Available stored credentials (use actual values in commands):\n"
|
732
|
+
|
733
|
+
for key, value in stored_credentials.items():
|
734
|
+
# Mask the actual value for security in logs, but provide the real value
|
735
|
+
masked_value = value[:8] + "..." if len(value) > 8 else "***"
|
736
|
+
auth_context += f"- {key}: {masked_value} (actual value: {value})\n"
|
737
|
+
|
738
|
+
return auth_context
|
739
|
+
|
330
740
|
def call_openai_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
|
331
741
|
"""Call OpenAI to debug a failed command and suggest a fix"""
|
332
742
|
print("\n🔍 DEBUG: Starting LLM debugging...")
|
@@ -620,6 +1030,9 @@ Directory contents:
|
|
620
1030
|
"Content-Type": "application/json",
|
621
1031
|
"Authorization": f"Bearer {api_key}"
|
622
1032
|
}
|
1033
|
+
|
1034
|
+
stored_credentials = get_stored_credentials()
|
1035
|
+
auth_context = generate_auth_context(stored_credentials)
|
623
1036
|
|
624
1037
|
# Create a prompt for the LLM
|
625
1038
|
print("\n" + "="*60)
|
@@ -644,29 +1057,41 @@ But it failed with this error:
|
|
644
1057
|
{directory_context}
|
645
1058
|
{file_context}
|
646
1059
|
|
1060
|
+
AVAILABLE CREDENTIALS:
|
1061
|
+
{auth_context}
|
1062
|
+
|
647
1063
|
Please analyze the error and provide ONLY a single terminal command that would fix the issue.
|
648
|
-
Consider the current directory, system information,
|
1064
|
+
Consider the current directory, system information, directory contents, and available credentials carefully before suggesting a solution.
|
649
1065
|
|
650
1066
|
IMPORTANT GUIDELINES:
|
651
1067
|
1. For any commands that might ask for yes/no confirmation, use the appropriate non-interactive flag:
|
652
1068
|
- For apt/apt-get: use -y or --yes
|
653
1069
|
- For pip: use --no-input
|
654
1070
|
- For rm: use -f or --force
|
655
|
-
- For other commands: check their documentation for the appropriate non-interactive flag
|
656
1071
|
|
657
|
-
2. If the error indicates a file is not found
|
1072
|
+
2. If the error indicates a file is not found:
|
658
1073
|
- FIRST try to search for the file using: find . -name "filename" -type f 2>/dev/null
|
659
1074
|
- If found, navigate to that directory using: cd /path/to/directory
|
660
1075
|
- If not found, then consider creating the file or installing missing packages
|
661
1076
|
|
662
1077
|
3. For missing packages or dependencies:
|
663
1078
|
- Use pip install for Python packages
|
664
|
-
- Use apt-get install for system packages
|
1079
|
+
- Use apt-get install -y for system packages
|
665
1080
|
- Use npm install for Node.js packages
|
666
1081
|
|
667
1082
|
4. For authentication issues:
|
668
|
-
-
|
669
|
-
-
|
1083
|
+
- Analyze the error to determine what type of authentication is needed
|
1084
|
+
- Use the actual credential values provided above (not placeholders)
|
1085
|
+
- Common patterns:
|
1086
|
+
* wandb errors: use wandb login with WANDB_API_KEY
|
1087
|
+
* huggingface errors: use huggingface-cli login with HF_TOKEN or HUGGINGFACE_TOKEN
|
1088
|
+
* github errors: configure git credentials with GITHUB_TOKEN
|
1089
|
+
* kaggle errors: create ~/.kaggle/kaggle.json with KAGGLE_USERNAME and KAGGLE_KEY
|
1090
|
+
* API errors: export the appropriate API key as environment variable
|
1091
|
+
|
1092
|
+
5. Environment variable exports:
|
1093
|
+
- Use export commands for API keys that need to be in environment
|
1094
|
+
- Use the actual credential values, not placeholders
|
670
1095
|
|
671
1096
|
Do not provide any explanations, just the exact command to run.
|
672
1097
|
"""
|
@@ -969,7 +1394,7 @@ def generate_random_password(length=16):
|
|
969
1394
|
return password
|
970
1395
|
|
971
1396
|
|
972
|
-
# Now modify the create_modal_ssh_container function to use the
|
1397
|
+
# Now modify the create_modal_ssh_container function to use the PersistentShell
|
973
1398
|
def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_commands=None,
|
974
1399
|
volume_name=None, timeout_minutes=60, ssh_password=None, interactive=False):
|
975
1400
|
"""Create a Modal SSH container with GPU support and tunneling"""
|
@@ -1263,6 +1688,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
1263
1688
|
subprocess.run(["service", "ssh", "start"], check=True)
|
1264
1689
|
|
1265
1690
|
# Clone repository if provided
|
1691
|
+
repo_dir = "/root"
|
1266
1692
|
if repo_url:
|
1267
1693
|
repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
|
1268
1694
|
print(f"📥 Cloning repository: {repo_url}")
|
@@ -1274,119 +1700,77 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
1274
1700
|
# Change to repository directory
|
1275
1701
|
repo_dir = f"/root/{repo_name_from_url}"
|
1276
1702
|
if os.path.exists(repo_dir):
|
1277
|
-
|
1278
|
-
print(f"📂 Changed to repository directory: {repo_dir}")
|
1703
|
+
print(f"📂 Will run setup commands in repository directory: {repo_dir}")
|
1279
1704
|
|
1280
1705
|
except subprocess.CalledProcessError as e:
|
1281
1706
|
print(f"❌ Failed to clone repository: {e}")
|
1282
1707
|
|
1283
|
-
# Run setup commands if provided
|
1708
|
+
# Run setup commands if provided using PersistentShell
|
1284
1709
|
if setup_commands:
|
1285
|
-
print(f"⚙️ Running {len(setup_commands)} setup commands...")
|
1710
|
+
print(f"⚙️ Running {len(setup_commands)} setup commands with persistent shell...")
|
1286
1711
|
|
1287
|
-
#
|
1288
|
-
|
1289
|
-
|
1290
|
-
|
1291
|
-
|
1292
|
-
|
1293
|
-
|
1294
|
-
|
1295
|
-
|
1296
|
-
|
1297
|
-
modified_cmd = cmd.replace("source ", ". ")
|
1298
|
-
# Wrap in bash -c to ensure it runs in bash
|
1299
|
-
bash_cmd = f"bash -c '{modified_cmd}'"
|
1300
|
-
print(f"🔄 Modified command: {bash_cmd}")
|
1301
|
-
result = subprocess.run(bash_cmd, shell=True, check=True,
|
1302
|
-
capture_output=True, text=True)
|
1303
|
-
else:
|
1304
|
-
result = subprocess.run(cmd, shell=True, check=True,
|
1305
|
-
capture_output=True, text=True)
|
1712
|
+
# Create persistent shell instance
|
1713
|
+
shell = PersistentShell(working_dir=repo_dir, timeout=120)
|
1714
|
+
|
1715
|
+
try:
|
1716
|
+
# Start the persistent shell
|
1717
|
+
shell.start()
|
1718
|
+
|
1719
|
+
# Execute each setup command
|
1720
|
+
for i, cmd in enumerate(setup_commands, 1):
|
1721
|
+
print(f"📋 Executing command {i}/{len(setup_commands)}: {cmd}")
|
1306
1722
|
|
1307
|
-
|
1308
|
-
print(f"✅ Output: {result.stdout}")
|
1309
|
-
return True, result.stdout, ""
|
1310
|
-
except subprocess.CalledProcessError as e:
|
1311
|
-
error_output = e.stderr if e.stderr else str(e)
|
1312
|
-
print(f"❌ Command failed: {e}")
|
1313
|
-
print(f"❌ Error: {error_output}")
|
1723
|
+
success, stdout, stderr = shell.execute(cmd, timeout=120)
|
1314
1724
|
|
1315
|
-
|
1316
|
-
|
1317
|
-
try:
|
1318
|
-
# Get the current directory for context
|
1319
|
-
current_dir = os.getcwd()
|
1725
|
+
if not success:
|
1726
|
+
print(f"⚠️ Command {i} failed, attempting LLM debugging...")
|
1320
1727
|
|
1321
1728
|
# Call OpenAI for debugging
|
1322
|
-
|
1323
|
-
|
1324
|
-
|
1325
|
-
print(f"🔍 DEBUG: Current directory: {current_dir}")
|
1326
|
-
|
1327
|
-
# Get the API key from environment or use the one that was fetched earlier
|
1328
|
-
api_key = os.environ.get("OPENAI_API_KEY")
|
1329
|
-
fix_command = call_openai_for_debug(cmd, error_output, api_key=api_key, current_dir=current_dir)
|
1330
|
-
|
1331
|
-
print(f"🔍 DEBUG: call_openai_for_debug returned: {fix_command}")
|
1332
|
-
|
1333
|
-
if fix_command:
|
1334
|
-
print(f"🔧 OpenAI suggested fix command: {fix_command}")
|
1729
|
+
try:
|
1730
|
+
current_dir = shell.get_cwd()
|
1731
|
+
api_key = os.environ.get("OPENAI_API_KEY")
|
1335
1732
|
|
1336
|
-
#
|
1337
|
-
|
1338
|
-
try:
|
1339
|
-
fix_result = subprocess.run(fix_command, shell=True, check=True,
|
1340
|
-
capture_output=True, text=True)
|
1341
|
-
if fix_result.stdout:
|
1342
|
-
print(f"✅ Fix command output: {fix_result.stdout}")
|
1343
|
-
|
1344
|
-
# Retry the original command
|
1345
|
-
print(f"🔄 Retrying original command: {cmd}")
|
1346
|
-
return run_command_with_basic_error_handling(cmd, show_output, retry_count + 1, max_retries)
|
1347
|
-
except subprocess.CalledProcessError as fix_e:
|
1348
|
-
print(f"❌ Fix command also failed: {fix_e}")
|
1349
|
-
return False, "", error_output
|
1350
|
-
else:
|
1351
|
-
print("❌ No fix suggested by OpenAI")
|
1352
|
-
return False, "", error_output
|
1733
|
+
# Use your existing call_openai_for_debug function
|
1734
|
+
fix_command = call_openai_for_debug(cmd, stderr, api_key=api_key, current_dir=current_dir, sandbox=shell)
|
1353
1735
|
|
1354
|
-
|
1355
|
-
|
1356
|
-
|
1357
|
-
|
1358
|
-
|
1359
|
-
|
1360
|
-
|
1361
|
-
|
1362
|
-
|
1363
|
-
|
1364
|
-
|
1365
|
-
|
1366
|
-
|
1367
|
-
|
1368
|
-
|
1369
|
-
|
1370
|
-
|
1371
|
-
|
1372
|
-
|
1373
|
-
|
1374
|
-
|
1736
|
+
if fix_command:
|
1737
|
+
print(f"🔧 OpenAI suggested fix command: {fix_command}")
|
1738
|
+
|
1739
|
+
# Run the fix command in the persistent shell
|
1740
|
+
print(f"🔄 Running suggested fix command: {fix_command}")
|
1741
|
+
fix_success, fix_stdout, fix_stderr = shell.execute(fix_command, timeout=120)
|
1742
|
+
|
1743
|
+
if fix_success:
|
1744
|
+
print(f"✅ Fix command succeeded")
|
1745
|
+
|
1746
|
+
# Retry the original command
|
1747
|
+
print(f"🔄 Retrying original command: {cmd}")
|
1748
|
+
retry_success, retry_stdout, retry_stderr = shell.execute(cmd, timeout=120)
|
1749
|
+
|
1750
|
+
if retry_success:
|
1751
|
+
print(f"✅ Original command succeeded after fix!")
|
1752
|
+
else:
|
1753
|
+
print(f"⚠️ Original command still failed after fix, continuing...")
|
1754
|
+
else:
|
1755
|
+
print(f"❌ Fix command failed: {fix_stderr}")
|
1756
|
+
print(f"⚠️ Continuing with remaining commands...")
|
1757
|
+
else:
|
1758
|
+
print("❌ No fix suggested by OpenAI")
|
1759
|
+
print(f"⚠️ Continuing with remaining commands...")
|
1375
1760
|
|
1376
|
-
|
1377
|
-
|
1378
|
-
|
1379
|
-
|
1380
|
-
|
1761
|
+
except Exception as debug_e:
|
1762
|
+
print(f"❌ LLM debugging failed: {debug_e}")
|
1763
|
+
print(f"⚠️ Continuing with remaining commands...")
|
1764
|
+
else:
|
1765
|
+
print(f"✅ Command {i} completed successfully")
|
1381
1766
|
|
1382
|
-
|
1383
|
-
|
1384
|
-
|
1385
|
-
|
1386
|
-
|
1387
|
-
|
1388
|
-
|
1389
|
-
subprocess.run("pwd && ls -la && echo '--- Parent directory ---' && ls -la ..", shell=True, check=False)
|
1767
|
+
print("✅ All setup commands processed")
|
1768
|
+
|
1769
|
+
except Exception as e:
|
1770
|
+
print(f"❌ Error during setup command execution: {e}")
|
1771
|
+
finally:
|
1772
|
+
# Clean up the shell
|
1773
|
+
shell.cleanup()
|
1390
1774
|
|
1391
1775
|
# Create SSH tunnel
|
1392
1776
|
with modal.forward(22, unencrypted=True) as tunnel:
|