gitarsenal-cli 1.9.74 โ 1.9.76
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/.venv_status.json +1 -1
- package/Step +0 -0
- package/kill_claude/claude_code_agent.py +22 -9
- package/package.json +1 -1
- package/python/__pycache__/credentials_manager.cpython-312.pyc +0 -0
- package/python/test_container_fail.py +239 -0
- package/python/test_container_pass.py +290 -0
- package/python/test_modalSandboxScript.py +408 -363
- package/python/test_container.py +0 -145
|
@@ -11,48 +11,6 @@ import string
|
|
|
11
11
|
import argparse
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
import modal
|
|
14
|
-
from auth_manager import AuthManager
|
|
15
|
-
|
|
16
|
-
# Removed unused boxed output functions since they're no longer used with the Agent-based approach
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# Removed _execute_with_box function as it's no longer used with the Agent-based approach
|
|
20
|
-
|
|
21
|
-
# Early argument parsing for proxy settings only
|
|
22
|
-
early_parser = argparse.ArgumentParser(add_help=False)
|
|
23
|
-
early_parser.add_argument('--proxy-url', help='URL of the proxy server')
|
|
24
|
-
early_parser.add_argument('--proxy-api-key', help='API key for the proxy server')
|
|
25
|
-
|
|
26
|
-
# Parse only proxy args early to avoid conflicts
|
|
27
|
-
early_args, _ = early_parser.parse_known_args()
|
|
28
|
-
|
|
29
|
-
# Set proxy URL and API key in environment variables if provided
|
|
30
|
-
if early_args.proxy_url:
|
|
31
|
-
os.environ["MODAL_PROXY_URL"] = early_args.proxy_url
|
|
32
|
-
|
|
33
|
-
if early_args.proxy_api_key:
|
|
34
|
-
os.environ["MODAL_PROXY_API_KEY"] = early_args.proxy_api_key
|
|
35
|
-
|
|
36
|
-
# Import the fetch_modal_tokens module
|
|
37
|
-
from fetch_modal_tokens import get_tokens
|
|
38
|
-
token_id, token_secret, openai_api_key, anthropic_api_key, openrouter_api_key, groq_api_key = get_tokens()
|
|
39
|
-
|
|
40
|
-
# Check if we got valid tokens
|
|
41
|
-
if token_id is None or token_secret is None:
|
|
42
|
-
raise ValueError("Could not get valid tokens")
|
|
43
|
-
|
|
44
|
-
# Explicitly set the environment variables again to be sure
|
|
45
|
-
os.environ["MODAL_TOKEN_ID"] = token_id
|
|
46
|
-
os.environ["MODAL_TOKEN_SECRET"] = token_secret
|
|
47
|
-
if openai_api_key:
|
|
48
|
-
os.environ["OPENAI_API_KEY"] = openai_api_key
|
|
49
|
-
if anthropic_api_key:
|
|
50
|
-
os.environ["ANTHROPIC_API_KEY"] = anthropic_api_key
|
|
51
|
-
# Also set the old environment variable for backward compatibility
|
|
52
|
-
os.environ["MODAL_TOKEN"] = token_id
|
|
53
|
-
|
|
54
|
-
# Set token variables for later use
|
|
55
|
-
token = token_id # For backward compatibility
|
|
56
14
|
|
|
57
15
|
|
|
58
16
|
def generate_random_password(length=16):
|
|
@@ -80,9 +38,297 @@ def get_stored_credentials():
|
|
|
80
38
|
return {}
|
|
81
39
|
|
|
82
40
|
|
|
41
|
+
# Global SSH container function (must be at global scope for Modal)
|
|
42
|
+
def ssh_container_function(ssh_password=None, repo_url=None, repo_name=None, setup_commands=None, openai_api_key=None, anthropic_api_key=None, stored_credentials=None):
|
|
43
|
+
"""Start SSH container with password authentication and intelligent repository setup using Agent."""
|
|
44
|
+
import subprocess
|
|
45
|
+
import time
|
|
46
|
+
import os
|
|
47
|
+
import json
|
|
48
|
+
import sys
|
|
49
|
+
import modal
|
|
50
|
+
|
|
51
|
+
print("๐ณ SSH Container Function Started!")
|
|
52
|
+
print(f"๐ Parameters received:")
|
|
53
|
+
print(f" - SSH Password: {'***' if ssh_password else 'None'}")
|
|
54
|
+
print(f" - Repo URL: {repo_url or 'None'}")
|
|
55
|
+
print(f" - Repo Name: {repo_name or 'None'}")
|
|
56
|
+
print(f" - Setup Commands: {len(setup_commands) if setup_commands else 0} commands")
|
|
57
|
+
print(f" - OpenAI API Key: {'Set' if openai_api_key else 'Not set'}")
|
|
58
|
+
print(f" - Anthropic API Key: {'Set' if anthropic_api_key else 'Not set'}")
|
|
59
|
+
print(f" - Stored Credentials: {len(stored_credentials) if stored_credentials else 0} items")
|
|
60
|
+
|
|
61
|
+
# Import only the modules we actually need (none currently for Agent-based approach)
|
|
62
|
+
# Note: CommandListManager and llm_debugging functions are not used in the Agent-based approach
|
|
63
|
+
print("โ
Container setup complete - using Agent-based repository setup")
|
|
64
|
+
|
|
65
|
+
# Set root password
|
|
66
|
+
subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
|
|
67
|
+
|
|
68
|
+
# Set OpenAI API key if provided
|
|
69
|
+
if openai_api_key:
|
|
70
|
+
os.environ['OPENAI_API_KEY'] = openai_api_key
|
|
71
|
+
else:
|
|
72
|
+
print("โ ๏ธ No OpenAI API key provided to container")
|
|
73
|
+
|
|
74
|
+
# Set up stored credentials in container environment
|
|
75
|
+
if stored_credentials:
|
|
76
|
+
print(f"๐ Setting up {len(stored_credentials)} stored credentials in container...")
|
|
77
|
+
for key, value in stored_credentials.items():
|
|
78
|
+
# Set each credential as an environment variable
|
|
79
|
+
env_var_name = key.upper().replace('-', '_').replace(' ', '_')
|
|
80
|
+
os.environ[env_var_name] = value
|
|
81
|
+
print(f"โ
Set {env_var_name} in container environment")
|
|
82
|
+
|
|
83
|
+
# Also save credentials to a file in the container for easy access
|
|
84
|
+
credentials_dir = "/root/.gitarsenal"
|
|
85
|
+
os.makedirs(credentials_dir, exist_ok=True)
|
|
86
|
+
credentials_file = os.path.join(credentials_dir, "credentials.json")
|
|
87
|
+
with open(credentials_file, 'w') as f:
|
|
88
|
+
json.dump(stored_credentials, f, indent=2)
|
|
89
|
+
print(f"โ
Saved credentials to {credentials_file}")
|
|
90
|
+
|
|
91
|
+
# Print available credentials for user reference
|
|
92
|
+
print("\n๐ AVAILABLE CREDENTIALS IN CONTAINER:")
|
|
93
|
+
print("="*50)
|
|
94
|
+
for key, value in stored_credentials.items():
|
|
95
|
+
masked_value = value[:8] + "..." if len(value) > 8 else "***"
|
|
96
|
+
env_var_name = key.upper().replace('-', '_').replace(' ', '_')
|
|
97
|
+
print(f" {key} -> {env_var_name} = {masked_value}")
|
|
98
|
+
print("="*50)
|
|
99
|
+
print("๐ก These credentials are available as environment variables and in /root/.gitarsenal/credentials.json")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# Start SSH service
|
|
103
|
+
subprocess.run(["service", "ssh", "start"], check=True)
|
|
104
|
+
|
|
105
|
+
# Use Agent for intelligent repository setup
|
|
106
|
+
if repo_url:
|
|
107
|
+
print("๐ค Using Agent for intelligent repository setup...")
|
|
108
|
+
|
|
109
|
+
# Set up environment variables for the Agent
|
|
110
|
+
if openai_api_key:
|
|
111
|
+
os.environ['OPENAI_API_KEY'] = openai_api_key
|
|
112
|
+
if anthropic_api_key:
|
|
113
|
+
os.environ['ANTHROPIC_API_KEY'] = anthropic_api_key
|
|
114
|
+
|
|
115
|
+
# Set up Anthropic API key from stored credentials
|
|
116
|
+
anthropic_api_key = None
|
|
117
|
+
if stored_credentials:
|
|
118
|
+
# Look for Anthropic API key in various possible names
|
|
119
|
+
for key_name in ['ANTHROPIC_API_KEY', 'anthropic_api_key', 'anthropic-api-key']:
|
|
120
|
+
if key_name in stored_credentials:
|
|
121
|
+
anthropic_api_key = stored_credentials[key_name]
|
|
122
|
+
os.environ['ANTHROPIC_API_KEY'] = anthropic_api_key
|
|
123
|
+
print(f"โ
Set Anthropic API key from stored credentials")
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
if not anthropic_api_key:
|
|
127
|
+
print("โ ๏ธ No Anthropic API key found in stored credentials")
|
|
128
|
+
print("๐ก Agent will require an Anthropic API key for operation")
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
print("๐ง Running Agent for repository setup...")
|
|
132
|
+
|
|
133
|
+
print("\n" + "="*80)
|
|
134
|
+
print("๐ค AGENT REPOSITORY SETUP")
|
|
135
|
+
print("="*80)
|
|
136
|
+
print(f"Repository: {repo_url}")
|
|
137
|
+
print(f"Working Directory: /root")
|
|
138
|
+
if stored_credentials:
|
|
139
|
+
print(f"Available Credentials: {len(stored_credentials)} items")
|
|
140
|
+
print("="*80 + "\n")
|
|
141
|
+
|
|
142
|
+
# Call Agent directly as subprocess with real-time output
|
|
143
|
+
claude_prompt = f"clone, setup and run {repo_url}"
|
|
144
|
+
print(f"๐ Executing the task: \"{claude_prompt}\"")
|
|
145
|
+
print("\n" + "="*60)
|
|
146
|
+
print("๐ AGENT OUTPUT (LIVE)")
|
|
147
|
+
print("="*60)
|
|
148
|
+
|
|
149
|
+
# Use Popen for real-time output streaming with optimizations
|
|
150
|
+
import sys
|
|
151
|
+
import select
|
|
152
|
+
import fcntl
|
|
153
|
+
import os as os_module
|
|
154
|
+
|
|
155
|
+
process = subprocess.Popen(
|
|
156
|
+
["python", "-u", "/python/kill_claude/claude_code_agent.py", claude_prompt], # -u for unbuffered output
|
|
157
|
+
cwd="/root",
|
|
158
|
+
stdout=subprocess.PIPE,
|
|
159
|
+
stderr=subprocess.PIPE, # Keep separate for better handling
|
|
160
|
+
text=True,
|
|
161
|
+
bufsize=0, # Unbuffered for fastest output
|
|
162
|
+
universal_newlines=True,
|
|
163
|
+
env=dict(os.environ, PYTHONUNBUFFERED='1') # Force unbuffered Python output
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Make stdout and stderr non-blocking for faster reading
|
|
167
|
+
def make_non_blocking(fd):
|
|
168
|
+
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
|
169
|
+
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os_module.O_NONBLOCK)
|
|
170
|
+
|
|
171
|
+
make_non_blocking(process.stdout)
|
|
172
|
+
make_non_blocking(process.stderr)
|
|
173
|
+
|
|
174
|
+
# Stream output in real-time with robust error handling
|
|
175
|
+
try:
|
|
176
|
+
stdout_buffer = ""
|
|
177
|
+
stderr_buffer = ""
|
|
178
|
+
|
|
179
|
+
while process.poll() is None:
|
|
180
|
+
try:
|
|
181
|
+
# Use select for efficient I/O multiplexing with error handling
|
|
182
|
+
ready, _, _ = select.select([process.stdout, process.stderr], [], [], 0.1) # 100ms timeout
|
|
183
|
+
|
|
184
|
+
for stream in ready:
|
|
185
|
+
try:
|
|
186
|
+
if stream == process.stdout:
|
|
187
|
+
chunk = stream.read(1024) # Read in chunks for efficiency
|
|
188
|
+
if chunk is not None and chunk:
|
|
189
|
+
stdout_buffer += chunk
|
|
190
|
+
# Process complete lines immediately
|
|
191
|
+
while '\n' in stdout_buffer:
|
|
192
|
+
line, stdout_buffer = stdout_buffer.split('\n', 1)
|
|
193
|
+
print(line, flush=True) # Force immediate flush
|
|
194
|
+
elif stream == process.stderr:
|
|
195
|
+
chunk = stream.read(1024)
|
|
196
|
+
if chunk is not None and chunk:
|
|
197
|
+
stderr_buffer += chunk
|
|
198
|
+
# Process complete lines immediately
|
|
199
|
+
while '\n' in stderr_buffer:
|
|
200
|
+
line, stderr_buffer = stderr_buffer.split('\n', 1)
|
|
201
|
+
print(f"STDERR: {line}", flush=True)
|
|
202
|
+
except (BlockingIOError, OSError, ValueError):
|
|
203
|
+
# Handle various I/O errors gracefully
|
|
204
|
+
continue
|
|
205
|
+
except (select.error, OSError):
|
|
206
|
+
# If select fails, fall back to simple polling
|
|
207
|
+
time.sleep(0.1)
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
# Process any remaining output after process ends
|
|
211
|
+
try:
|
|
212
|
+
# Read any remaining data from streams
|
|
213
|
+
remaining_stdout = process.stdout.read()
|
|
214
|
+
remaining_stderr = process.stderr.read()
|
|
215
|
+
|
|
216
|
+
if remaining_stdout:
|
|
217
|
+
stdout_buffer += remaining_stdout
|
|
218
|
+
if remaining_stderr:
|
|
219
|
+
stderr_buffer += remaining_stderr
|
|
220
|
+
|
|
221
|
+
# Output remaining buffered content
|
|
222
|
+
if stdout_buffer.strip():
|
|
223
|
+
print(stdout_buffer.strip(), flush=True)
|
|
224
|
+
if stderr_buffer.strip():
|
|
225
|
+
print(f"STDERR: {stderr_buffer.strip()}", flush=True)
|
|
226
|
+
except (OSError, ValueError):
|
|
227
|
+
# Handle cases where streams are already closed
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
# Get final return code
|
|
231
|
+
return_code = process.returncode
|
|
232
|
+
|
|
233
|
+
print("\n" + "="*60)
|
|
234
|
+
if return_code == 0:
|
|
235
|
+
print("โ
Agent completed successfully!")
|
|
236
|
+
else:
|
|
237
|
+
print(f"โ ๏ธ Agent exited with code: {return_code}")
|
|
238
|
+
print("="*60)
|
|
239
|
+
|
|
240
|
+
except subprocess.TimeoutExpired:
|
|
241
|
+
print("\nโ ๏ธ Agent timed out after 10 minutes")
|
|
242
|
+
process.kill()
|
|
243
|
+
process.wait()
|
|
244
|
+
except Exception as stream_error:
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
# Fallback to simple readline approach
|
|
248
|
+
try:
|
|
249
|
+
# Restart the process with simpler streaming
|
|
250
|
+
if process.poll() is None:
|
|
251
|
+
process.kill()
|
|
252
|
+
process.wait()
|
|
253
|
+
|
|
254
|
+
fallback_process = subprocess.Popen(
|
|
255
|
+
["python", "-u", "/python/kill_claude/claude_code_agent.py", claude_prompt],
|
|
256
|
+
cwd="/root",
|
|
257
|
+
stdout=subprocess.PIPE,
|
|
258
|
+
stderr=subprocess.STDOUT,
|
|
259
|
+
text=True,
|
|
260
|
+
bufsize=1,
|
|
261
|
+
universal_newlines=True
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Simple line-by-line reading
|
|
265
|
+
while True:
|
|
266
|
+
line = fallback_process.stdout.readline()
|
|
267
|
+
if line == '' and fallback_process.poll() is not None:
|
|
268
|
+
break
|
|
269
|
+
if line:
|
|
270
|
+
print(line.rstrip(), flush=True)
|
|
271
|
+
|
|
272
|
+
return_code = fallback_process.returncode
|
|
273
|
+
|
|
274
|
+
print("\n" + "="*60)
|
|
275
|
+
if return_code == 0:
|
|
276
|
+
print("โ
Agent completed successfully!")
|
|
277
|
+
else:
|
|
278
|
+
print(f"โ ๏ธ Agent exited with code: {return_code}")
|
|
279
|
+
print("="*60)
|
|
280
|
+
|
|
281
|
+
except Exception as fallback_error:
|
|
282
|
+
print(f"\nโ Fallback streaming also failed: {fallback_error}")
|
|
283
|
+
print("โ ๏ธ Agent may have completed, but output streaming failed")
|
|
284
|
+
return_code = 1
|
|
285
|
+
|
|
286
|
+
except Exception as e:
|
|
287
|
+
print(f"โ Error during repository setup: {e}")
|
|
288
|
+
print("โ ๏ธ Proceeding without setup...")
|
|
289
|
+
import traceback
|
|
290
|
+
traceback.print_exc()
|
|
291
|
+
else:
|
|
292
|
+
print("โ ๏ธ No repository URL provided, skipping setup")
|
|
293
|
+
|
|
294
|
+
print("๐ Creating SSH tunnel on port 22...")
|
|
295
|
+
# Create SSH tunnel
|
|
296
|
+
with modal.forward(22, unencrypted=True) as tunnel:
|
|
297
|
+
host, port = tunnel.tcp_socket
|
|
298
|
+
|
|
299
|
+
print("\n" + "=" * 80)
|
|
300
|
+
print("๐ SSH CONTAINER IS READY!")
|
|
301
|
+
print("=" * 80)
|
|
302
|
+
print(f"๐ SSH Host: {host}")
|
|
303
|
+
print(f"๐ SSH Port: {port}")
|
|
304
|
+
print(f"๐ค Username: root")
|
|
305
|
+
print(f"๐ Password: {ssh_password}")
|
|
306
|
+
print()
|
|
307
|
+
print("๐ CONNECT USING THIS COMMAND:")
|
|
308
|
+
print(f"ssh -p {port} root@{host}")
|
|
309
|
+
print("=" * 80)
|
|
310
|
+
|
|
311
|
+
print("๐ Starting keep-alive loop...")
|
|
312
|
+
# Keep the container running
|
|
313
|
+
iteration = 0
|
|
314
|
+
while True:
|
|
315
|
+
iteration += 1
|
|
316
|
+
if iteration % 10 == 1: # Print every 5 minutes (10 * 30 seconds = 5 minutes)
|
|
317
|
+
print(f"๐ Container alive (iteration {iteration})")
|
|
318
|
+
|
|
319
|
+
time.sleep(30)
|
|
320
|
+
# Check if SSH service is still running
|
|
321
|
+
try:
|
|
322
|
+
subprocess.run(["service", "ssh", "status"], check=True,
|
|
323
|
+
capture_output=True)
|
|
324
|
+
except subprocess.CalledProcessError:
|
|
325
|
+
print("โ ๏ธ SSH service stopped, restarting...")
|
|
326
|
+
subprocess.run(["service", "ssh", "start"], check=True)
|
|
327
|
+
|
|
328
|
+
|
|
83
329
|
# Create Modal SSH container with GPU support and intelligent repository setup using Agent
|
|
84
330
|
def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_commands=None,
|
|
85
|
-
volume_name=None, timeout_minutes=60, ssh_password=None, interactive=False, gpu_count=1
|
|
331
|
+
volume_name=None, timeout_minutes=60, ssh_password=None, interactive=False, gpu_count=1):
|
|
86
332
|
"""Create a Modal SSH container with GPU support and intelligent repository setup.
|
|
87
333
|
|
|
88
334
|
When repo_url is provided, uses Agent for intelligent repository setup.
|
|
@@ -240,25 +486,20 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
240
486
|
|
|
241
487
|
# Choose base image to avoid CUDA segfault issues
|
|
242
488
|
print("โ ๏ธ Using CUDA base image - this may cause segfaults on some systems")
|
|
243
|
-
|
|
244
|
-
base_image = modal.Image.debian_slim()
|
|
245
|
-
pip_cmd = "uv_pip_install"
|
|
489
|
+
base_image = modal.Image.from_registry("nvidia/cuda:12.4.0-devel-ubuntu22.04", add_python="3.11")
|
|
490
|
+
# base_image = modal.Image.debian_slim()
|
|
246
491
|
|
|
247
492
|
# Build the SSH image with the chosen base
|
|
248
493
|
ssh_image = (
|
|
249
494
|
base_image
|
|
250
495
|
.apt_install(
|
|
251
496
|
"openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
|
|
252
|
-
"python3", "python3-pip"
|
|
253
|
-
"gpg", "ca-certificates", "software-properties-common"
|
|
497
|
+
"python3", "python3-pip"
|
|
254
498
|
)
|
|
255
499
|
)
|
|
256
500
|
|
|
257
501
|
# Add Python packages using the appropriate method
|
|
258
|
-
|
|
259
|
-
ssh_image = ssh_image.uv_pip_install("uv", "modal", "gitingest", "requests", "openai", "anthropic", "exa-py")
|
|
260
|
-
else:
|
|
261
|
-
ssh_image = ssh_image.pip_install("modal", "gitingest", "requests", "openai", "anthropic", "exa-py")
|
|
502
|
+
ssh_image = ssh_image.uv_pip_install("uv", "modal", "gitingest", "requests", "openai", "anthropic", "exa-py")
|
|
262
503
|
|
|
263
504
|
# Add the rest of the configuration
|
|
264
505
|
ssh_image = ssh_image.run_commands(
|
|
@@ -266,21 +507,11 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
266
507
|
"mkdir -p /var/run/sshd",
|
|
267
508
|
"mkdir -p /root/.ssh",
|
|
268
509
|
"chmod 700 /root/.ssh",
|
|
269
|
-
|
|
270
|
-
|
|
510
|
+
|
|
511
|
+
"ssh-keygen -A",
|
|
271
512
|
"sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
|
|
272
513
|
"sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
|
|
273
|
-
"
|
|
274
|
-
|
|
275
|
-
# SSH keep-alive settings
|
|
276
|
-
"echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
|
|
277
|
-
"echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
|
|
278
|
-
|
|
279
|
-
# Generate SSH host keys
|
|
280
|
-
"ssh-keygen -A",
|
|
281
|
-
|
|
282
|
-
# Set up a nice bash prompt
|
|
283
|
-
"echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
|
|
514
|
+
"echo 'export PATH=/usr/local/cuda/bin:$PATH' >> /root/.bashrc"
|
|
284
515
|
|
|
285
516
|
# Create base directories (subdirectories will be created automatically when mounting)
|
|
286
517
|
"mkdir -p /python",
|
|
@@ -293,306 +524,27 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
293
524
|
volumes_config[volume_mount_path] = volume
|
|
294
525
|
|
|
295
526
|
# Create app with image passed directly (THIS IS THE KEY CHANGE)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
print("โ
Created app successfully")
|
|
300
|
-
except Exception as e:
|
|
301
|
-
print(f"โ Error creating app: {e}")
|
|
302
|
-
return None
|
|
527
|
+
print("๐ Testing app creation...")
|
|
528
|
+
app = modal.App(app_name, image=ssh_image) # Pass image here
|
|
529
|
+
print("โ
Created app successfully")
|
|
303
530
|
|
|
304
|
-
#
|
|
305
|
-
|
|
531
|
+
# Apply the decorator to the global SSH container function
|
|
532
|
+
decorated_ssh_function = app.function(
|
|
306
533
|
timeout=timeout_minutes * 60, # Convert to seconds
|
|
307
534
|
gpu=gpu_spec['modal_gpu'], # Use the user-selected GPU type and count
|
|
308
|
-
serialized=True,
|
|
309
535
|
volumes=volumes_config if volumes_config else None,
|
|
310
|
-
)
|
|
311
|
-
def ssh_container_function(ssh_password=None, repo_url=None, repo_name=None, setup_commands=None, openai_api_key=None, anthropic_api_key=None, stored_credentials=None):
|
|
312
|
-
"""Start SSH container with password authentication and intelligent repository setup using Agent."""
|
|
313
|
-
import subprocess
|
|
314
|
-
import time
|
|
315
|
-
import os
|
|
316
|
-
import json
|
|
317
|
-
import sys
|
|
318
|
-
|
|
319
|
-
# Add the mounted python directory to the Python path
|
|
320
|
-
# sys.path.insert(0, "/python")
|
|
321
|
-
|
|
322
|
-
# Import only the modules we actually need (none currently for Agent-based approach)
|
|
323
|
-
# Note: CommandListManager and llm_debugging functions are not used in the Agent-based approach
|
|
324
|
-
print("โ
Container setup complete - using Agent-based repository setup")
|
|
325
|
-
|
|
326
|
-
# Set root password
|
|
327
|
-
subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
|
|
328
|
-
|
|
329
|
-
# Set OpenAI API key if provided
|
|
330
|
-
if openai_api_key:
|
|
331
|
-
os.environ['OPENAI_API_KEY'] = openai_api_key
|
|
332
|
-
else:
|
|
333
|
-
print("โ ๏ธ No OpenAI API key provided to container")
|
|
334
|
-
|
|
335
|
-
# Set up stored credentials in container environment
|
|
336
|
-
if stored_credentials:
|
|
337
|
-
print(f"๐ Setting up {len(stored_credentials)} stored credentials in container...")
|
|
338
|
-
for key, value in stored_credentials.items():
|
|
339
|
-
# Set each credential as an environment variable
|
|
340
|
-
env_var_name = key.upper().replace('-', '_').replace(' ', '_')
|
|
341
|
-
os.environ[env_var_name] = value
|
|
342
|
-
print(f"โ
Set {env_var_name} in container environment")
|
|
343
|
-
|
|
344
|
-
# Also save credentials to a file in the container for easy access
|
|
345
|
-
try:
|
|
346
|
-
credentials_dir = "/root/.gitarsenal"
|
|
347
|
-
os.makedirs(credentials_dir, exist_ok=True)
|
|
348
|
-
credentials_file = os.path.join(credentials_dir, "credentials.json")
|
|
349
|
-
with open(credentials_file, 'w') as f:
|
|
350
|
-
json.dump(stored_credentials, f, indent=2)
|
|
351
|
-
print(f"โ
Saved credentials to {credentials_file}")
|
|
352
|
-
|
|
353
|
-
# Print available credentials for user reference
|
|
354
|
-
print("\n๐ AVAILABLE CREDENTIALS IN CONTAINER:")
|
|
355
|
-
print("="*50)
|
|
356
|
-
for key, value in stored_credentials.items():
|
|
357
|
-
masked_value = value[:8] + "..." if len(value) > 8 else "***"
|
|
358
|
-
env_var_name = key.upper().replace('-', '_').replace(' ', '_')
|
|
359
|
-
print(f" {key} -> {env_var_name} = {masked_value}")
|
|
360
|
-
print("="*50)
|
|
361
|
-
print("๐ก These credentials are available as environment variables and in /root/.gitarsenal/credentials.json")
|
|
362
|
-
|
|
363
|
-
except Exception as e:
|
|
364
|
-
print(f"โ ๏ธ Could not save credentials file: {e}")
|
|
365
|
-
else:
|
|
366
|
-
print("โ ๏ธ No stored credentials provided to container")
|
|
367
|
-
|
|
368
|
-
# Start SSH service
|
|
369
|
-
subprocess.run(["service", "ssh", "start"], check=True)
|
|
370
|
-
|
|
371
|
-
# Use Agent for intelligent repository setup
|
|
372
|
-
if repo_url:
|
|
373
|
-
print("๐ค Using Agent for intelligent repository setup...")
|
|
374
|
-
|
|
375
|
-
# Set up environment variables for the Agent
|
|
376
|
-
if openai_api_key:
|
|
377
|
-
os.environ['OPENAI_API_KEY'] = openai_api_key
|
|
378
|
-
if anthropic_api_key:
|
|
379
|
-
os.environ['ANTHROPIC_API_KEY'] = anthropic_api_key
|
|
380
|
-
|
|
381
|
-
# Set up Anthropic API key from stored credentials
|
|
382
|
-
anthropic_api_key = None
|
|
383
|
-
if stored_credentials:
|
|
384
|
-
# Look for Anthropic API key in various possible names
|
|
385
|
-
for key_name in ['ANTHROPIC_API_KEY', 'anthropic_api_key', 'anthropic-api-key']:
|
|
386
|
-
if key_name in stored_credentials:
|
|
387
|
-
anthropic_api_key = stored_credentials[key_name]
|
|
388
|
-
os.environ['ANTHROPIC_API_KEY'] = anthropic_api_key
|
|
389
|
-
print(f"โ
Set Anthropic API key from stored credentials")
|
|
390
|
-
break
|
|
391
|
-
|
|
392
|
-
if not anthropic_api_key:
|
|
393
|
-
print("โ ๏ธ No Anthropic API key found in stored credentials")
|
|
394
|
-
print("๐ก Agent will require an Anthropic API key for operation")
|
|
395
|
-
|
|
396
|
-
try:
|
|
397
|
-
print("๐ง Running Agent for repository setup...")
|
|
398
|
-
|
|
399
|
-
print("\n" + "="*80)
|
|
400
|
-
print("๐ค AGENT REPOSITORY SETUP")
|
|
401
|
-
print("="*80)
|
|
402
|
-
print(f"Repository: {repo_url}")
|
|
403
|
-
print(f"Working Directory: /root")
|
|
404
|
-
if stored_credentials:
|
|
405
|
-
print(f"Available Credentials: {len(stored_credentials)} items")
|
|
406
|
-
print("="*80 + "\n")
|
|
407
|
-
|
|
408
|
-
# Call Agent directly as subprocess with real-time output
|
|
409
|
-
claude_prompt = f"clone, setup and run {repo_url}"
|
|
410
|
-
print(f"๐ Executing the task: \"{claude_prompt}\"")
|
|
411
|
-
print("\n" + "="*60)
|
|
412
|
-
print("๐ AGENT OUTPUT (LIVE)")
|
|
413
|
-
print("="*60)
|
|
414
|
-
|
|
415
|
-
# Use Popen for real-time output streaming with optimizations
|
|
416
|
-
import sys
|
|
417
|
-
import select
|
|
418
|
-
import fcntl
|
|
419
|
-
import os as os_module
|
|
420
|
-
|
|
421
|
-
process = subprocess.Popen(
|
|
422
|
-
["python", "-u", "/python/kill_claude/claude_code_agent.py", claude_prompt], # -u for unbuffered output
|
|
423
|
-
cwd="/root",
|
|
424
|
-
stdout=subprocess.PIPE,
|
|
425
|
-
stderr=subprocess.PIPE, # Keep separate for better handling
|
|
426
|
-
text=True,
|
|
427
|
-
bufsize=0, # Unbuffered for fastest output
|
|
428
|
-
universal_newlines=True,
|
|
429
|
-
env=dict(os.environ, PYTHONUNBUFFERED='1') # Force unbuffered Python output
|
|
430
|
-
)
|
|
431
|
-
|
|
432
|
-
# Make stdout and stderr non-blocking for faster reading
|
|
433
|
-
def make_non_blocking(fd):
|
|
434
|
-
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
|
435
|
-
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os_module.O_NONBLOCK)
|
|
436
|
-
|
|
437
|
-
make_non_blocking(process.stdout)
|
|
438
|
-
make_non_blocking(process.stderr)
|
|
439
|
-
|
|
440
|
-
# Stream output in real-time with robust error handling
|
|
441
|
-
try:
|
|
442
|
-
stdout_buffer = ""
|
|
443
|
-
stderr_buffer = ""
|
|
444
|
-
|
|
445
|
-
while process.poll() is None:
|
|
446
|
-
try:
|
|
447
|
-
# Use select for efficient I/O multiplexing with error handling
|
|
448
|
-
ready, _, _ = select.select([process.stdout, process.stderr], [], [], 0.1) # 100ms timeout
|
|
449
|
-
|
|
450
|
-
for stream in ready:
|
|
451
|
-
try:
|
|
452
|
-
if stream == process.stdout:
|
|
453
|
-
chunk = stream.read(1024) # Read in chunks for efficiency
|
|
454
|
-
if chunk is not None and chunk:
|
|
455
|
-
stdout_buffer += chunk
|
|
456
|
-
# Process complete lines immediately
|
|
457
|
-
while '\n' in stdout_buffer:
|
|
458
|
-
line, stdout_buffer = stdout_buffer.split('\n', 1)
|
|
459
|
-
print(line, flush=True) # Force immediate flush
|
|
460
|
-
elif stream == process.stderr:
|
|
461
|
-
chunk = stream.read(1024)
|
|
462
|
-
if chunk is not None and chunk:
|
|
463
|
-
stderr_buffer += chunk
|
|
464
|
-
# Process complete lines immediately
|
|
465
|
-
while '\n' in stderr_buffer:
|
|
466
|
-
line, stderr_buffer = stderr_buffer.split('\n', 1)
|
|
467
|
-
print(f"STDERR: {line}", flush=True)
|
|
468
|
-
except (BlockingIOError, OSError, ValueError):
|
|
469
|
-
# Handle various I/O errors gracefully
|
|
470
|
-
continue
|
|
471
|
-
except (select.error, OSError):
|
|
472
|
-
# If select fails, fall back to simple polling
|
|
473
|
-
time.sleep(0.1)
|
|
474
|
-
continue
|
|
475
|
-
|
|
476
|
-
# Process any remaining output after process ends
|
|
477
|
-
try:
|
|
478
|
-
# Read any remaining data from streams
|
|
479
|
-
remaining_stdout = process.stdout.read()
|
|
480
|
-
remaining_stderr = process.stderr.read()
|
|
481
|
-
|
|
482
|
-
if remaining_stdout:
|
|
483
|
-
stdout_buffer += remaining_stdout
|
|
484
|
-
if remaining_stderr:
|
|
485
|
-
stderr_buffer += remaining_stderr
|
|
486
|
-
|
|
487
|
-
# Output remaining buffered content
|
|
488
|
-
if stdout_buffer.strip():
|
|
489
|
-
print(stdout_buffer.strip(), flush=True)
|
|
490
|
-
if stderr_buffer.strip():
|
|
491
|
-
print(f"STDERR: {stderr_buffer.strip()}", flush=True)
|
|
492
|
-
except (OSError, ValueError):
|
|
493
|
-
# Handle cases where streams are already closed
|
|
494
|
-
pass
|
|
495
|
-
|
|
496
|
-
# Get final return code
|
|
497
|
-
return_code = process.returncode
|
|
498
|
-
|
|
499
|
-
print("\n" + "="*60)
|
|
500
|
-
if return_code == 0:
|
|
501
|
-
print("โ
Agent completed successfully!")
|
|
502
|
-
else:
|
|
503
|
-
print(f"โ ๏ธ Agent exited with code: {return_code}")
|
|
504
|
-
print("="*60)
|
|
505
|
-
|
|
506
|
-
except subprocess.TimeoutExpired:
|
|
507
|
-
print("\nโ ๏ธ Agent timed out after 10 minutes")
|
|
508
|
-
process.kill()
|
|
509
|
-
process.wait()
|
|
510
|
-
except Exception as stream_error:
|
|
511
|
-
pass
|
|
512
|
-
|
|
513
|
-
# Fallback to simple readline approach
|
|
514
|
-
try:
|
|
515
|
-
# Restart the process with simpler streaming
|
|
516
|
-
if process.poll() is None:
|
|
517
|
-
process.kill()
|
|
518
|
-
process.wait()
|
|
519
|
-
|
|
520
|
-
fallback_process = subprocess.Popen(
|
|
521
|
-
["python", "-u", "/python/kill_claude/claude_code_agent.py", claude_prompt],
|
|
522
|
-
cwd="/root",
|
|
523
|
-
stdout=subprocess.PIPE,
|
|
524
|
-
stderr=subprocess.STDOUT,
|
|
525
|
-
text=True,
|
|
526
|
-
bufsize=1,
|
|
527
|
-
universal_newlines=True
|
|
528
|
-
)
|
|
529
|
-
|
|
530
|
-
# Simple line-by-line reading
|
|
531
|
-
while True:
|
|
532
|
-
line = fallback_process.stdout.readline()
|
|
533
|
-
if line == '' and fallback_process.poll() is not None:
|
|
534
|
-
break
|
|
535
|
-
if line:
|
|
536
|
-
print(line.rstrip(), flush=True)
|
|
537
|
-
|
|
538
|
-
return_code = fallback_process.returncode
|
|
539
|
-
|
|
540
|
-
print("\n" + "="*60)
|
|
541
|
-
if return_code == 0:
|
|
542
|
-
print("โ
Agent completed successfully!")
|
|
543
|
-
else:
|
|
544
|
-
print(f"โ ๏ธ Agent exited with code: {return_code}")
|
|
545
|
-
print("="*60)
|
|
546
|
-
|
|
547
|
-
except Exception as fallback_error:
|
|
548
|
-
print(f"\nโ Fallback streaming also failed: {fallback_error}")
|
|
549
|
-
print("โ ๏ธ Agent may have completed, but output streaming failed")
|
|
550
|
-
return_code = 1
|
|
551
|
-
|
|
552
|
-
except Exception as e:
|
|
553
|
-
print(f"โ Error during repository setup: {e}")
|
|
554
|
-
print("โ ๏ธ Proceeding without setup...")
|
|
555
|
-
import traceback
|
|
556
|
-
traceback.print_exc()
|
|
557
|
-
else:
|
|
558
|
-
print("โ ๏ธ No repository URL provided, skipping setup")
|
|
559
|
-
|
|
560
|
-
# Create SSH tunnel
|
|
561
|
-
with modal.forward(22, unencrypted=True) as tunnel:
|
|
562
|
-
host, port = tunnel.tcp_socket
|
|
563
|
-
|
|
564
|
-
print("\n" + "=" * 80)
|
|
565
|
-
print("๐ SSH CONTAINER IS READY!")
|
|
566
|
-
print("=" * 80)
|
|
567
|
-
print(f"๐ SSH Host: {host}")
|
|
568
|
-
print(f"๐ SSH Port: {port}")
|
|
569
|
-
print(f"๐ค Username: root")
|
|
570
|
-
print(f"๐ Password: {ssh_password}")
|
|
571
|
-
print()
|
|
572
|
-
print("๐ CONNECT USING THIS COMMAND:")
|
|
573
|
-
print(f"ssh -p {port} root@{host}")
|
|
574
|
-
print("=" * 80)
|
|
575
|
-
|
|
576
|
-
# Keep the container running
|
|
577
|
-
while True:
|
|
578
|
-
time.sleep(30)
|
|
579
|
-
# Check if SSH service is still running
|
|
580
|
-
try:
|
|
581
|
-
subprocess.run(["service", "ssh", "status"], check=True,
|
|
582
|
-
capture_output=True)
|
|
583
|
-
except subprocess.CalledProcessError:
|
|
584
|
-
print("โ ๏ธ SSH service stopped, restarting...")
|
|
585
|
-
subprocess.run(["service", "ssh", "start"], check=True)
|
|
536
|
+
)(ssh_container_function)
|
|
586
537
|
|
|
587
538
|
# Run the container
|
|
588
539
|
try:
|
|
589
540
|
print("โณ Starting container... This may take 1-2 minutes...")
|
|
590
541
|
|
|
591
|
-
# Start the container
|
|
542
|
+
# Start the container and wait for it to complete (blocking)
|
|
592
543
|
with modal.enable_output():
|
|
593
544
|
with app.run():
|
|
594
545
|
# Get the API key from environment
|
|
595
|
-
|
|
546
|
+
openai_api_key = os.environ.get("OPENAI_API_KEY")
|
|
547
|
+
anthropic_api_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
596
548
|
|
|
597
549
|
# Get stored credentials from local file
|
|
598
550
|
stored_credentials = get_stored_credentials()
|
|
@@ -601,9 +553,69 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
601
553
|
else:
|
|
602
554
|
print("โ ๏ธ No stored credentials found")
|
|
603
555
|
|
|
604
|
-
|
|
556
|
+
# Use spawn() to get a FunctionCall handle, then wait for it
|
|
557
|
+
print("๐ Spawning SSH container...")
|
|
558
|
+
try:
|
|
559
|
+
function_call = decorated_ssh_function.spawn(ssh_password, repo_url, repo_name, setup_commands, openai_api_key, anthropic_api_key, stored_credentials)
|
|
560
|
+
print(f"โ
Container spawned with call ID: {function_call.object_id}")
|
|
561
|
+
print(f"๐ Function call status: {function_call}")
|
|
562
|
+
except Exception as spawn_error:
|
|
563
|
+
print(f"โ Error during spawn: {spawn_error}")
|
|
564
|
+
raise
|
|
565
|
+
|
|
566
|
+
try:
|
|
567
|
+
# Wait for the function to start and print connection info (with timeout)
|
|
568
|
+
print("โณ Waiting for container to initialize...")
|
|
569
|
+
|
|
570
|
+
# Use a timeout to see if the container is starting properly
|
|
571
|
+
print("๐ Checking container status with 30-second timeout...")
|
|
572
|
+
try:
|
|
573
|
+
result = function_call.get(timeout=30)
|
|
574
|
+
print(f"๐ Container function completed with result: {result}")
|
|
575
|
+
except TimeoutError:
|
|
576
|
+
print("โฐ Container is still running after 30 seconds - this is expected!")
|
|
577
|
+
print("๐ฏ The container should be accessible via SSH now.")
|
|
578
|
+
print("๐ก The function will continue running until manually stopped.")
|
|
579
|
+
print("๐ Use Ctrl+C to stop monitoring, but the container will keep running.")
|
|
580
|
+
print("๐ Keeping tokens active since container is still running.")
|
|
581
|
+
|
|
582
|
+
# Continue waiting for user interrupt
|
|
583
|
+
try:
|
|
584
|
+
print("\nโณ Monitoring container (press Ctrl+C to stop monitoring)...")
|
|
585
|
+
result = function_call.get() # Wait indefinitely
|
|
586
|
+
print(f"๐ Container function completed with result: {result}")
|
|
587
|
+
except KeyboardInterrupt:
|
|
588
|
+
print("\n๐ Stopped monitoring. Container is still running remotely.")
|
|
589
|
+
print("๐ก Use Modal's web UI or CLI to stop the container when done.")
|
|
590
|
+
print("๐ Keeping tokens active since container is still running.")
|
|
591
|
+
return {
|
|
592
|
+
"app_name": app_name,
|
|
593
|
+
"ssh_password": ssh_password,
|
|
594
|
+
"volume_name": volume_name,
|
|
595
|
+
"status": "monitoring_stopped",
|
|
596
|
+
"function_call_id": function_call.object_id
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
except KeyboardInterrupt:
|
|
600
|
+
print("\n๐ Interrupted by user. Container may still be running remotely.")
|
|
601
|
+
print("๐ก Use Modal's web UI or CLI to check running containers.")
|
|
602
|
+
print("๐ Keeping tokens active since container may still be running.")
|
|
603
|
+
return {
|
|
604
|
+
"app_name": app_name,
|
|
605
|
+
"ssh_password": ssh_password,
|
|
606
|
+
"volume_name": volume_name,
|
|
607
|
+
"status": "interrupted",
|
|
608
|
+
"function_call_id": function_call.object_id
|
|
609
|
+
}
|
|
610
|
+
except Exception as e:
|
|
611
|
+
print(f"โ ๏ธ Container execution error: {e}")
|
|
612
|
+
print("๐ก Container may still be accessible via SSH if it started successfully.")
|
|
613
|
+
print("๐งน Cleaning up tokens due to execution error.")
|
|
614
|
+
cleanup_modal_token()
|
|
615
|
+
raise
|
|
605
616
|
|
|
606
|
-
#
|
|
617
|
+
# Only clean up tokens if container actually completed normally
|
|
618
|
+
print("๐งน Container completed normally, cleaning up tokens.")
|
|
607
619
|
cleanup_modal_token()
|
|
608
620
|
|
|
609
621
|
return {
|
|
@@ -904,13 +916,10 @@ if __name__ == "__main__":
|
|
|
904
916
|
parser.add_argument('--list-gpus', action='store_true', help='List available GPU types with their specifications')
|
|
905
917
|
parser.add_argument('--interactive', action='store_true', help='Run in interactive mode with prompts')
|
|
906
918
|
parser.add_argument('--yes', action='store_true', help='Automatically confirm prompts (non-interactive)')
|
|
907
|
-
|
|
908
|
-
parser.add_argument('--proxy-url', help='URL of the proxy server')
|
|
909
|
-
parser.add_argument('--proxy-api-key', help='API key for the proxy server')
|
|
919
|
+
|
|
910
920
|
parser.add_argument('--gpu', default='A10G', help='GPU type to use')
|
|
911
921
|
parser.add_argument('--gpu-count', type=int, default=1, help='Number of GPUs to use (default: 1)')
|
|
912
922
|
parser.add_argument('--repo-url', help='Repository URL')
|
|
913
|
-
parser.add_argument('--use-cuda-base', action='store_true', help='Use CUDA base image (may cause segfaults, use only if needed for CUDA libraries)')
|
|
914
923
|
|
|
915
924
|
# Authentication-related arguments
|
|
916
925
|
parser.add_argument('--auth', action='store_true', help='Manage authentication (login, register, logout)')
|
|
@@ -930,7 +939,29 @@ if __name__ == "__main__":
|
|
|
930
939
|
|
|
931
940
|
args = parser.parse_args()
|
|
932
941
|
|
|
933
|
-
# Initialize
|
|
942
|
+
# Initialize tokens (import here to avoid container import issues)
|
|
943
|
+
from fetch_modal_tokens import get_tokens
|
|
944
|
+
token_id, token_secret, openai_api_key, anthropic_api_key, openrouter_api_key, groq_api_key = get_tokens()
|
|
945
|
+
|
|
946
|
+
# Check if we got valid tokens
|
|
947
|
+
if token_id is None or token_secret is None:
|
|
948
|
+
raise ValueError("Could not get valid tokens")
|
|
949
|
+
|
|
950
|
+
# Explicitly set the environment variables again to be sure
|
|
951
|
+
os.environ["MODAL_TOKEN_ID"] = token_id
|
|
952
|
+
os.environ["MODAL_TOKEN_SECRET"] = token_secret
|
|
953
|
+
if openai_api_key:
|
|
954
|
+
os.environ["OPENAI_API_KEY"] = openai_api_key
|
|
955
|
+
if anthropic_api_key:
|
|
956
|
+
os.environ["ANTHROPIC_API_KEY"] = anthropic_api_key
|
|
957
|
+
# Also set the old environment variable for backward compatibility
|
|
958
|
+
os.environ["MODAL_TOKEN"] = token_id
|
|
959
|
+
|
|
960
|
+
# Set token variables for later use
|
|
961
|
+
token = token_id # For backward compatibility
|
|
962
|
+
|
|
963
|
+
# Initialize authentication manager (import here to avoid container import issues)
|
|
964
|
+
from auth_manager import AuthManager
|
|
934
965
|
auth_manager = AuthManager()
|
|
935
966
|
|
|
936
967
|
# Handle authentication-related commands
|
|
@@ -1191,7 +1222,7 @@ if __name__ == "__main__":
|
|
|
1191
1222
|
repo_name = repo_name[:-4]
|
|
1192
1223
|
|
|
1193
1224
|
# Create the container
|
|
1194
|
-
create_modal_ssh_container(
|
|
1225
|
+
result = create_modal_ssh_container(
|
|
1195
1226
|
gpu_type=args.gpu,
|
|
1196
1227
|
repo_url=args.repo_url,
|
|
1197
1228
|
repo_name=repo_name,
|
|
@@ -1201,10 +1232,24 @@ if __name__ == "__main__":
|
|
|
1201
1232
|
ssh_password=ssh_password,
|
|
1202
1233
|
interactive=args.interactive,
|
|
1203
1234
|
gpu_count=getattr(args, 'gpu_count', 1),
|
|
1204
|
-
use_cuda_base=getattr(args, 'use_cuda_base', False)
|
|
1205
1235
|
)
|
|
1236
|
+
|
|
1237
|
+
if result:
|
|
1238
|
+
print(f"\nโ
Container operation completed: {result.get('status', 'success')}")
|
|
1239
|
+
if result.get('function_call_id'):
|
|
1240
|
+
print(f"๐ Function Call ID: {result['function_call_id']}")
|
|
1241
|
+
print("๐ก You can use this ID to check container status via Modal CLI")
|
|
1242
|
+
else:
|
|
1243
|
+
print("\nโ Container creation failed")
|
|
1244
|
+
|
|
1206
1245
|
except KeyboardInterrupt:
|
|
1246
|
+
print("\n๐ Operation cancelled by user")
|
|
1207
1247
|
cleanup_modal_token()
|
|
1208
1248
|
sys.exit(1)
|
|
1209
1249
|
except Exception as e:
|
|
1210
|
-
|
|
1250
|
+
print(f"\nโ Unexpected error: {e}")
|
|
1251
|
+
print("๐ Error details:")
|
|
1252
|
+
import traceback
|
|
1253
|
+
traceback.print_exc()
|
|
1254
|
+
cleanup_modal_token()
|
|
1255
|
+
sys.exit(1)
|