gitarsenal-cli 1.9.21 → 1.9.24
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/package.json +1 -1
- package/python/__pycache__/auth_manager.cpython-313.pyc +0 -0
- package/python/__pycache__/command_manager.cpython-313.pyc +0 -0
- package/python/__pycache__/fetch_modal_tokens.cpython-313.pyc +0 -0
- package/python/__pycache__/llm_debugging.cpython-313.pyc +0 -0
- package/python/__pycache__/modal_container.cpython-313.pyc +0 -0
- package/python/__pycache__/shell.cpython-313.pyc +0 -0
- package/python/api_integration.py +0 -0
- package/python/command_manager.py +613 -0
- package/python/credentials_manager.py +0 -0
- package/python/fetch_modal_tokens.py +0 -0
- package/python/fix_modal_token.py +0 -0
- package/python/fix_modal_token_advanced.py +0 -0
- package/python/gitarsenal.py +0 -0
- package/python/gitarsenal_proxy_client.py +0 -0
- package/python/llm_debugging.py +1369 -0
- package/python/modal_container.py +626 -0
- package/python/setup.py +15 -0
- package/python/setup_modal_token.py +0 -39
- package/python/shell.py +627 -0
- package/python/test_modalSandboxScript.py +75 -2639
- package/scripts/postinstall.js +22 -23
- package/python/__pycache__/credentials_manager.cpython-313.pyc +0 -0
- package/python/__pycache__/test_modalSandboxScript.cpython-313.pyc +0 -0
- package/python/__pycache__/test_modalSandboxScript_stable.cpython-313.pyc +0 -0
- package/python/debug_delete.py +0 -167
- package/python/documentation.py +0 -76
- package/python/fix_setup_commands.py +0 -116
- package/python/modal_auth_patch.py +0 -178
- package/python/modal_proxy_service.py +0 -665
- package/python/modal_token_solution.py +0 -293
- package/python/test_dynamic_commands.py +0 -147
- package/test_modalSandboxScript.py +0 -5004
|
@@ -1,665 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Modal Proxy Service for GitArsenal CLI
|
|
4
|
-
|
|
5
|
-
This service allows GitArsenal CLI users to access Modal services without exposing your Modal token.
|
|
6
|
-
It acts as a secure proxy between clients and Modal's API.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import os
|
|
10
|
-
import json
|
|
11
|
-
import secrets
|
|
12
|
-
import string
|
|
13
|
-
import time
|
|
14
|
-
from flask import Flask, request, jsonify
|
|
15
|
-
from flask_cors import CORS
|
|
16
|
-
import modal
|
|
17
|
-
import threading
|
|
18
|
-
import logging
|
|
19
|
-
from dotenv import load_dotenv
|
|
20
|
-
import uuid
|
|
21
|
-
import sys
|
|
22
|
-
from pathlib import Path
|
|
23
|
-
import subprocess
|
|
24
|
-
import signal
|
|
25
|
-
|
|
26
|
-
# Add the current directory to the path so we can import the test_modalSandboxScript module
|
|
27
|
-
current_dir = Path(__file__).parent.absolute()
|
|
28
|
-
sys.path.append(str(current_dir.parent))
|
|
29
|
-
|
|
30
|
-
# Load environment variables from .env file
|
|
31
|
-
load_dotenv()
|
|
32
|
-
|
|
33
|
-
# Configure logging
|
|
34
|
-
logging.basicConfig(
|
|
35
|
-
level=logging.INFO,
|
|
36
|
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
37
|
-
handlers=[
|
|
38
|
-
logging.FileHandler("modal_proxy.log"),
|
|
39
|
-
logging.StreamHandler()
|
|
40
|
-
]
|
|
41
|
-
)
|
|
42
|
-
logger = logging.getLogger("modal-proxy")
|
|
43
|
-
|
|
44
|
-
app = Flask(__name__)
|
|
45
|
-
CORS(app) # Enable CORS for all routes
|
|
46
|
-
|
|
47
|
-
# Set up the built-in Modal token
|
|
48
|
-
try:
|
|
49
|
-
from setup_modal_token import setup_modal_token, BUILT_IN_MODAL_TOKEN
|
|
50
|
-
token_setup_success = setup_modal_token()
|
|
51
|
-
if token_setup_success:
|
|
52
|
-
logger.info("Modal token set up successfully using setup_modal_token module")
|
|
53
|
-
else:
|
|
54
|
-
logger.warning("setup_modal_token module failed to set up Modal token")
|
|
55
|
-
|
|
56
|
-
# Always set the built-in token
|
|
57
|
-
MODAL_TOKEN = BUILT_IN_MODAL_TOKEN
|
|
58
|
-
except ImportError:
|
|
59
|
-
logger.warning("setup_modal_token module not found")
|
|
60
|
-
# Fallback to a hardcoded token
|
|
61
|
-
# Modal tokens are in the format: ak-xxxxxxxxxxxxxxxxxx
|
|
62
|
-
MODAL_TOKEN = "ak-eNMIXRdfbvpxIXcSHKPFQW" # Your actual token
|
|
63
|
-
except Exception as e:
|
|
64
|
-
logger.error(f"Error using setup_modal_token module: {e}")
|
|
65
|
-
# Fallback to a hardcoded token
|
|
66
|
-
# Modal tokens are in the format: ak-xxxxxxxxxxxxxxxxxx
|
|
67
|
-
MODAL_TOKEN = "ak-eNMIXRdfbvpxIXcSHKPFQW" # Your actual token
|
|
68
|
-
|
|
69
|
-
# Set the token in environment variables
|
|
70
|
-
os.environ["MODAL_TOKEN"] = MODAL_TOKEN
|
|
71
|
-
os.environ["MODAL_TOKEN_ID"] = MODAL_TOKEN
|
|
72
|
-
logger.info(f"Using built-in Modal token (length: {len(MODAL_TOKEN)})")
|
|
73
|
-
|
|
74
|
-
# Dictionary to store active containers
|
|
75
|
-
active_containers = {}
|
|
76
|
-
|
|
77
|
-
# Authentication tokens for clients
|
|
78
|
-
# In a production environment, use a proper authentication system
|
|
79
|
-
API_KEYS = {
|
|
80
|
-
"gitarsenal-default-key": True # Default key for local development
|
|
81
|
-
}
|
|
82
|
-
if os.environ.get("API_KEYS"):
|
|
83
|
-
# Add any additional keys from environment
|
|
84
|
-
for key in os.environ.get("API_KEYS").split(","):
|
|
85
|
-
API_KEYS[key.strip()] = True
|
|
86
|
-
|
|
87
|
-
def generate_api_key():
|
|
88
|
-
"""Generate a new API key for a client"""
|
|
89
|
-
return ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(32))
|
|
90
|
-
|
|
91
|
-
def authenticate_request():
|
|
92
|
-
"""Authenticate the request using API key"""
|
|
93
|
-
api_key = request.headers.get('X-API-Key')
|
|
94
|
-
|
|
95
|
-
# For development/testing: Accept requests without API key from localhost
|
|
96
|
-
if request.remote_addr in ['127.0.0.1', 'localhost']:
|
|
97
|
-
logger.info(f"Allowing request from localhost without API key check")
|
|
98
|
-
return True
|
|
99
|
-
|
|
100
|
-
# Normal API key check for other requests
|
|
101
|
-
if not api_key or api_key not in API_KEYS:
|
|
102
|
-
logger.warning(f"Authentication failed: Invalid or missing API key")
|
|
103
|
-
return False
|
|
104
|
-
|
|
105
|
-
logger.info(f"Request authenticated with API key")
|
|
106
|
-
return True
|
|
107
|
-
|
|
108
|
-
def generate_random_password(length=16):
|
|
109
|
-
"""Generate a random password for SSH access"""
|
|
110
|
-
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
|
|
111
|
-
password = ''.join(secrets.choice(alphabet) for i in range(length))
|
|
112
|
-
return password
|
|
113
|
-
|
|
114
|
-
def setup_modal_auth():
|
|
115
|
-
"""Set up Modal authentication using the server's token"""
|
|
116
|
-
# First, try to fetch tokens from the proxy server
|
|
117
|
-
try:
|
|
118
|
-
# Import the fetch_modal_tokens module
|
|
119
|
-
logger.info("Fetching Modal tokens from proxy server...")
|
|
120
|
-
from fetch_modal_tokens import get_tokens
|
|
121
|
-
token_id, token_secret, _, _ = get_tokens()
|
|
122
|
-
logger.info("Modal tokens fetched successfully")
|
|
123
|
-
|
|
124
|
-
# Set the tokens in environment variables
|
|
125
|
-
os.environ["MODAL_TOKEN_ID"] = token_id
|
|
126
|
-
os.environ["MODAL_TOKEN_SECRET"] = token_secret
|
|
127
|
-
|
|
128
|
-
# Create token files
|
|
129
|
-
modal_dir = Path.home() / ".modal"
|
|
130
|
-
modal_dir.mkdir(exist_ok=True)
|
|
131
|
-
token_file = modal_dir / "token.json"
|
|
132
|
-
with open(token_file, 'w') as f:
|
|
133
|
-
json.dump({
|
|
134
|
-
"token_id": token_id,
|
|
135
|
-
"token_secret": token_secret
|
|
136
|
-
}, f)
|
|
137
|
-
logger.info(f"Created token file at {token_file}")
|
|
138
|
-
|
|
139
|
-
# Create .modalconfig file
|
|
140
|
-
modalconfig_file = Path.home() / ".modalconfig"
|
|
141
|
-
with open(modalconfig_file, 'w') as f:
|
|
142
|
-
f.write(f"token_id = {token_id}\n")
|
|
143
|
-
f.write(f"token_secret = {token_secret}\n")
|
|
144
|
-
logger.info(f"Created .modalconfig file at {modalconfig_file}")
|
|
145
|
-
|
|
146
|
-
# Verify token is working by attempting a simple operation
|
|
147
|
-
try:
|
|
148
|
-
# Try a simple Modal operation
|
|
149
|
-
import modal.cli.app_create
|
|
150
|
-
logger.info("Modal module imported successfully")
|
|
151
|
-
return True
|
|
152
|
-
except Exception as e:
|
|
153
|
-
logger.error(f"Error importing Modal module: {e}")
|
|
154
|
-
# Fall back to other methods
|
|
155
|
-
except Exception as e:
|
|
156
|
-
logger.error(f"Error fetching Modal tokens: {e}")
|
|
157
|
-
# Fall back to other methods
|
|
158
|
-
|
|
159
|
-
# Use the comprehensive Modal token solution as fallback
|
|
160
|
-
try:
|
|
161
|
-
# Import the comprehensive solution module
|
|
162
|
-
logger.info("Applying comprehensive Modal token solution...")
|
|
163
|
-
import modal_token_solution
|
|
164
|
-
logger.info("Comprehensive Modal token solution applied")
|
|
165
|
-
|
|
166
|
-
# Verify token is working by attempting a simple operation
|
|
167
|
-
try:
|
|
168
|
-
# Try a simple Modal operation
|
|
169
|
-
import modal.cli.app_create
|
|
170
|
-
logger.info("Modal module imported successfully")
|
|
171
|
-
return True
|
|
172
|
-
except Exception as e:
|
|
173
|
-
logger.error(f"Error importing Modal module: {e}")
|
|
174
|
-
|
|
175
|
-
# Fall back to the authentication patch
|
|
176
|
-
try:
|
|
177
|
-
logger.info("Falling back to Modal authentication patch...")
|
|
178
|
-
import modal_auth_patch
|
|
179
|
-
logger.info("Modal authentication patch applied")
|
|
180
|
-
return True
|
|
181
|
-
except Exception as patch_e:
|
|
182
|
-
logger.error(f"Error applying Modal authentication patch: {patch_e}")
|
|
183
|
-
|
|
184
|
-
# Fall back to fix_modal_token.py
|
|
185
|
-
try:
|
|
186
|
-
# Execute the fix_modal_token.py script
|
|
187
|
-
logger.info("Falling back to fix_modal_token.py...")
|
|
188
|
-
result = subprocess.run(
|
|
189
|
-
["python", os.path.join(os.path.dirname(__file__), "fix_modal_token.py")],
|
|
190
|
-
capture_output=True,
|
|
191
|
-
text=True
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
# Log the output
|
|
195
|
-
for line in result.stdout.splitlines():
|
|
196
|
-
logger.info(f"fix_modal_token.py: {line}")
|
|
197
|
-
|
|
198
|
-
if result.returncode != 0:
|
|
199
|
-
logger.warning(f"fix_modal_token.py exited with code {result.returncode}")
|
|
200
|
-
if result.stderr:
|
|
201
|
-
logger.error(f"fix_modal_token.py error: {result.stderr}")
|
|
202
|
-
return False
|
|
203
|
-
|
|
204
|
-
logger.info("Modal token setup completed via fix_modal_token.py")
|
|
205
|
-
return True
|
|
206
|
-
except Exception as token_e:
|
|
207
|
-
logger.error(f"Error running fix_modal_token.py: {token_e}")
|
|
208
|
-
return False
|
|
209
|
-
except Exception as e:
|
|
210
|
-
logger.error(f"Error setting up Modal authentication: {e}")
|
|
211
|
-
return False
|
|
212
|
-
|
|
213
|
-
def cleanup_security_tokens():
|
|
214
|
-
"""Delete all security tokens and API keys after SSH container is started"""
|
|
215
|
-
logger.info("🧹 Cleaning up security tokens and API keys...")
|
|
216
|
-
|
|
217
|
-
try:
|
|
218
|
-
# Remove Modal tokens from environment variables
|
|
219
|
-
modal_env_vars = ["MODAL_TOKEN_ID", "MODAL_TOKEN", "MODAL_TOKEN_SECRET"]
|
|
220
|
-
for var in modal_env_vars:
|
|
221
|
-
if var in os.environ:
|
|
222
|
-
del os.environ[var]
|
|
223
|
-
logger.info(f"✅ Removed {var} from environment")
|
|
224
|
-
|
|
225
|
-
# Remove OpenAI API key from environment
|
|
226
|
-
if "OPENAI_API_KEY" in os.environ:
|
|
227
|
-
del os.environ["OPENAI_API_KEY"]
|
|
228
|
-
logger.info("✅ Removed OpenAI API key from environment")
|
|
229
|
-
|
|
230
|
-
# Delete ~/.modal.toml file
|
|
231
|
-
from pathlib import Path
|
|
232
|
-
modal_toml = Path.home() / ".modal.toml"
|
|
233
|
-
if modal_toml.exists():
|
|
234
|
-
modal_toml.unlink()
|
|
235
|
-
logger.info(f"✅ Deleted Modal token file at {modal_toml}")
|
|
236
|
-
|
|
237
|
-
# Delete ~/.gitarsenal/openai_key file
|
|
238
|
-
openai_key_file = Path.home() / ".gitarsenal" / "openai_key"
|
|
239
|
-
if openai_key_file.exists():
|
|
240
|
-
openai_key_file.unlink()
|
|
241
|
-
logger.info(f"✅ Deleted OpenAI API key file at {openai_key_file}")
|
|
242
|
-
|
|
243
|
-
logger.info("✅ Security cleanup completed successfully")
|
|
244
|
-
except Exception as e:
|
|
245
|
-
logger.error(f"❌ Error during security cleanup: {e}")
|
|
246
|
-
|
|
247
|
-
# Keep the old function for backward compatibility
|
|
248
|
-
def cleanup_modal_token():
|
|
249
|
-
"""Legacy function - now calls the comprehensive cleanup"""
|
|
250
|
-
cleanup_security_tokens()
|
|
251
|
-
|
|
252
|
-
@app.route('/api/health', methods=['GET'])
|
|
253
|
-
def health_check():
|
|
254
|
-
"""Health check endpoint"""
|
|
255
|
-
# Add CORS headers manually for this endpoint to ensure it works with direct browser requests
|
|
256
|
-
response = jsonify({"status": "ok", "message": "Modal proxy service is running"})
|
|
257
|
-
response.headers.add('Access-Control-Allow-Origin', '*')
|
|
258
|
-
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization,X-API-Key')
|
|
259
|
-
response.headers.add('Access-Control-Allow-Methods', 'GET,POST,OPTIONS')
|
|
260
|
-
return response
|
|
261
|
-
|
|
262
|
-
@app.route('/', methods=['GET'])
|
|
263
|
-
def root():
|
|
264
|
-
"""Root endpoint for basic connectivity testing"""
|
|
265
|
-
return jsonify({"status": "ok", "message": "Modal proxy service is running"})
|
|
266
|
-
|
|
267
|
-
@app.route('/api/modal-tokens', methods=['GET'])
|
|
268
|
-
def get_modal_tokens():
|
|
269
|
-
"""Get Modal tokens (protected by API key)"""
|
|
270
|
-
if not authenticate_request():
|
|
271
|
-
return jsonify({"error": "Unauthorized"}), 401
|
|
272
|
-
|
|
273
|
-
# Return the server's Modal token
|
|
274
|
-
return jsonify({
|
|
275
|
-
"token_id": MODAL_TOKEN,
|
|
276
|
-
"token_secret": MODAL_TOKEN # For compatibility, use the same token
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
@app.route('/api/create-api-key', methods=['POST'])
|
|
280
|
-
def create_api_key():
|
|
281
|
-
"""Create a new API key (protected by admin key)"""
|
|
282
|
-
admin_key = request.headers.get('X-Admin-Key')
|
|
283
|
-
if not admin_key or admin_key != os.environ.get("ADMIN_KEY"):
|
|
284
|
-
return jsonify({"error": "Unauthorized"}), 401
|
|
285
|
-
|
|
286
|
-
new_key = generate_api_key()
|
|
287
|
-
API_KEYS[new_key] = True
|
|
288
|
-
|
|
289
|
-
return jsonify({"api_key": new_key})
|
|
290
|
-
|
|
291
|
-
@app.route('/api/create-sandbox', methods=['POST'])
|
|
292
|
-
def create_sandbox():
|
|
293
|
-
"""Create a Modal sandbox"""
|
|
294
|
-
if not authenticate_request():
|
|
295
|
-
return jsonify({"error": "Unauthorized"}), 401
|
|
296
|
-
|
|
297
|
-
if not setup_modal_auth():
|
|
298
|
-
return jsonify({"error": "Failed to set up Modal authentication"}), 500
|
|
299
|
-
|
|
300
|
-
try:
|
|
301
|
-
data = request.json
|
|
302
|
-
gpu_type = data.get('gpu_type', 'A10G')
|
|
303
|
-
repo_url = data.get('repo_url')
|
|
304
|
-
repo_name = data.get('repo_name')
|
|
305
|
-
setup_commands = data.get('setup_commands', [])
|
|
306
|
-
volume_name = data.get('volume_name')
|
|
307
|
-
|
|
308
|
-
# Import the sandbox creation function from your module
|
|
309
|
-
from test_modalSandboxScript import create_modal_sandbox
|
|
310
|
-
|
|
311
|
-
logger.info(f"Creating sandbox with GPU: {gpu_type}, Repo: {repo_url}")
|
|
312
|
-
|
|
313
|
-
# Create a unique ID for this sandbox
|
|
314
|
-
sandbox_id = str(uuid.uuid4())
|
|
315
|
-
|
|
316
|
-
# Start sandbox creation in a separate thread
|
|
317
|
-
def create_sandbox_thread():
|
|
318
|
-
try:
|
|
319
|
-
# Ensure Modal token is set before creating sandbox
|
|
320
|
-
if not setup_modal_auth():
|
|
321
|
-
logger.error("Failed to set up Modal authentication in thread")
|
|
322
|
-
return
|
|
323
|
-
|
|
324
|
-
result = create_modal_sandbox(
|
|
325
|
-
gpu_type,
|
|
326
|
-
repo_url=repo_url,
|
|
327
|
-
repo_name=repo_name,
|
|
328
|
-
setup_commands=setup_commands,
|
|
329
|
-
volume_name=volume_name
|
|
330
|
-
)
|
|
331
|
-
|
|
332
|
-
if result:
|
|
333
|
-
active_containers[sandbox_id] = {
|
|
334
|
-
"container_id": result.get("container_id"),
|
|
335
|
-
"sandbox_id": result.get("sandbox_id"),
|
|
336
|
-
"created_at": time.time(),
|
|
337
|
-
"type": "sandbox"
|
|
338
|
-
}
|
|
339
|
-
logger.info(f"Sandbox created successfully: {result.get('container_id')}")
|
|
340
|
-
else:
|
|
341
|
-
logger.error("Failed to create sandbox")
|
|
342
|
-
except Exception as e:
|
|
343
|
-
logger.error(f"Error in sandbox creation thread: {e}")
|
|
344
|
-
|
|
345
|
-
thread = threading.Thread(target=create_sandbox_thread)
|
|
346
|
-
thread.daemon = True
|
|
347
|
-
thread.start()
|
|
348
|
-
|
|
349
|
-
return jsonify({
|
|
350
|
-
"message": "Sandbox creation started",
|
|
351
|
-
"sandbox_id": sandbox_id
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
except Exception as e:
|
|
355
|
-
logger.error(f"Error creating sandbox: {e}")
|
|
356
|
-
return jsonify({"error": str(e)}), 500
|
|
357
|
-
|
|
358
|
-
@app.route('/api/create-ssh-container', methods=['POST'])
|
|
359
|
-
def create_ssh_container():
|
|
360
|
-
"""Create a Modal SSH container"""
|
|
361
|
-
if not authenticate_request():
|
|
362
|
-
return jsonify({"error": "Unauthorized"}), 401
|
|
363
|
-
|
|
364
|
-
# Check if Modal token is available
|
|
365
|
-
if not MODAL_TOKEN:
|
|
366
|
-
logger.error("Cannot create SSH container: No Modal token available")
|
|
367
|
-
return jsonify({"error": "Modal token not configured on server"}), 500
|
|
368
|
-
|
|
369
|
-
try:
|
|
370
|
-
# Set the token directly in environment
|
|
371
|
-
os.environ["MODAL_TOKEN_ID"] = MODAL_TOKEN
|
|
372
|
-
os.environ["MODAL_TOKEN"] = MODAL_TOKEN
|
|
373
|
-
logger.info(f"Set MODAL_TOKEN_ID in environment (length: {len(MODAL_TOKEN)})")
|
|
374
|
-
|
|
375
|
-
# Create the token file directly in the expected location
|
|
376
|
-
try:
|
|
377
|
-
from pathlib import Path
|
|
378
|
-
modal_dir = Path.home() / ".modal"
|
|
379
|
-
modal_dir.mkdir(exist_ok=True)
|
|
380
|
-
token_file = modal_dir / "token.json"
|
|
381
|
-
with open(token_file, 'w') as f:
|
|
382
|
-
f.write(f'{{"token_id": "{MODAL_TOKEN}", "token": "{MODAL_TOKEN}"}}')
|
|
383
|
-
logger.info(f"Created Modal token file at {token_file}")
|
|
384
|
-
|
|
385
|
-
# Set up token using multiple approaches
|
|
386
|
-
# 1. Create .modalconfig file as an alternative method
|
|
387
|
-
modalconfig_file = Path.home() / ".modalconfig"
|
|
388
|
-
with open(modalconfig_file, 'w') as f:
|
|
389
|
-
f.write(f"token_id = {token}\n")
|
|
390
|
-
logger.info(f"Created .modalconfig file at {modalconfig_file}")
|
|
391
|
-
|
|
392
|
-
# 2. Import modal and set token directly
|
|
393
|
-
import modal
|
|
394
|
-
|
|
395
|
-
# 3. Try to directly configure Modal
|
|
396
|
-
try:
|
|
397
|
-
import modal.config
|
|
398
|
-
modal.config._auth_config.token_id = token
|
|
399
|
-
logger.info("Explicitly set token in Modal config")
|
|
400
|
-
except Exception as e:
|
|
401
|
-
logger.warning(f"Error setting token in Modal config: {e}")
|
|
402
|
-
# No need to clean up any temporary files
|
|
403
|
-
|
|
404
|
-
if result.returncode == 0:
|
|
405
|
-
logger.info("Successfully set token via Modal CLI")
|
|
406
|
-
else:
|
|
407
|
-
logger.warning(f"Modal CLI token set returned: {result.stderr}")
|
|
408
|
-
|
|
409
|
-
# As a fallback, also create the token file in the expected location
|
|
410
|
-
try:
|
|
411
|
-
from pathlib import Path
|
|
412
|
-
modal_dir = Path.home() / ".modal"
|
|
413
|
-
modal_dir.mkdir(exist_ok=True)
|
|
414
|
-
token_file = modal_dir / "token.json"
|
|
415
|
-
with open(token_file, 'w') as f:
|
|
416
|
-
f.write(f'{{"token_id": "{MODAL_TOKEN}", "token": "{MODAL_TOKEN}"}}')
|
|
417
|
-
logger.info(f"Created Modal token file at {token_file}")
|
|
418
|
-
except Exception as file_err:
|
|
419
|
-
logger.warning(f"Failed to create Modal token file: {file_err}")
|
|
420
|
-
except Exception as e:
|
|
421
|
-
logger.warning(f"Failed to set token via CLI: {e}")
|
|
422
|
-
|
|
423
|
-
data = request.json
|
|
424
|
-
gpu_type = data.get('gpu_type', 'A10G')
|
|
425
|
-
repo_url = data.get('repo_url')
|
|
426
|
-
repo_name = data.get('repo_name')
|
|
427
|
-
setup_commands = data.get('setup_commands', [])
|
|
428
|
-
volume_name = data.get('volume_name')
|
|
429
|
-
timeout_minutes = data.get('timeout', 60)
|
|
430
|
-
|
|
431
|
-
# Generate a random password for SSH
|
|
432
|
-
ssh_password = generate_random_password()
|
|
433
|
-
|
|
434
|
-
# Import the SSH container creation function
|
|
435
|
-
from test_modalSandboxScript import create_modal_ssh_container
|
|
436
|
-
|
|
437
|
-
logger.info(f"Creating SSH container with GPU: {gpu_type}, Repo: {repo_url}")
|
|
438
|
-
|
|
439
|
-
# Create a unique ID for this container
|
|
440
|
-
container_id = str(uuid.uuid4())
|
|
441
|
-
|
|
442
|
-
# Log token status for debugging
|
|
443
|
-
token_status = "Token is set" if MODAL_TOKEN else "Token is missing"
|
|
444
|
-
logger.info(f"Modal token status before thread: {token_status}")
|
|
445
|
-
logger.info(f"Modal token length: {len(MODAL_TOKEN) if MODAL_TOKEN else 'N/A'}")
|
|
446
|
-
|
|
447
|
-
# Start container creation in a separate thread
|
|
448
|
-
def create_container_thread():
|
|
449
|
-
try:
|
|
450
|
-
# Set token in environment for this thread
|
|
451
|
-
os.environ["MODAL_TOKEN_ID"] = MODAL_TOKEN
|
|
452
|
-
os.environ["MODAL_TOKEN"] = MODAL_TOKEN
|
|
453
|
-
logger.info(f"Set MODAL_TOKEN_ID in thread (length: {len(MODAL_TOKEN)})")
|
|
454
|
-
|
|
455
|
-
# Create a copy of the environment variables for the container creation
|
|
456
|
-
env_copy = os.environ.copy()
|
|
457
|
-
env_copy["MODAL_TOKEN_ID"] = MODAL_TOKEN
|
|
458
|
-
env_copy["MODAL_TOKEN"] = MODAL_TOKEN
|
|
459
|
-
|
|
460
|
-
# First, try to fetch tokens from the proxy server
|
|
461
|
-
try:
|
|
462
|
-
# Import the fetch_modal_tokens module
|
|
463
|
-
logger.info("Fetching Modal tokens from proxy server in thread...")
|
|
464
|
-
from fetch_modal_tokens import get_tokens
|
|
465
|
-
token_id, token_secret, _, _ = get_tokens()
|
|
466
|
-
logger.info("Modal tokens fetched successfully in thread")
|
|
467
|
-
|
|
468
|
-
# Set the tokens in environment variables
|
|
469
|
-
os.environ["MODAL_TOKEN_ID"] = token_id
|
|
470
|
-
os.environ["MODAL_TOKEN_SECRET"] = token_secret
|
|
471
|
-
|
|
472
|
-
# Create token files
|
|
473
|
-
modal_dir = Path.home() / ".modal"
|
|
474
|
-
modal_dir.mkdir(exist_ok=True)
|
|
475
|
-
token_file = modal_dir / "token.json"
|
|
476
|
-
with open(token_file, 'w') as f:
|
|
477
|
-
json.dump({
|
|
478
|
-
"token_id": token_id,
|
|
479
|
-
"token_secret": token_secret
|
|
480
|
-
}, f)
|
|
481
|
-
logger.info(f"Created token file at {token_file} in thread")
|
|
482
|
-
|
|
483
|
-
# Create .modalconfig file
|
|
484
|
-
modalconfig_file = Path.home() / ".modalconfig"
|
|
485
|
-
with open(modalconfig_file, 'w') as f:
|
|
486
|
-
f.write(f"token_id = {token_id}\n")
|
|
487
|
-
f.write(f"token_secret = {token_secret}\n")
|
|
488
|
-
logger.info(f"Created .modalconfig file at {modalconfig_file} in thread")
|
|
489
|
-
except Exception as e:
|
|
490
|
-
logger.error(f"Error fetching Modal tokens in thread: {e}")
|
|
491
|
-
|
|
492
|
-
# Fall back to the comprehensive Modal token solution
|
|
493
|
-
try:
|
|
494
|
-
# Import the comprehensive solution module
|
|
495
|
-
logger.info("Applying comprehensive Modal token solution in thread...")
|
|
496
|
-
import modal_token_solution
|
|
497
|
-
logger.info("Comprehensive Modal token solution applied in thread")
|
|
498
|
-
except Exception as e:
|
|
499
|
-
logger.error(f"Error applying comprehensive Modal token solution in thread: {e}")
|
|
500
|
-
|
|
501
|
-
# Fall back to the authentication patch
|
|
502
|
-
try:
|
|
503
|
-
logger.info("Falling back to Modal authentication patch in thread...")
|
|
504
|
-
import modal_auth_patch
|
|
505
|
-
logger.info("Modal authentication patch applied in thread")
|
|
506
|
-
except Exception as patch_e:
|
|
507
|
-
logger.error(f"Error applying Modal authentication patch in thread: {patch_e}")
|
|
508
|
-
|
|
509
|
-
# Fall back to fix_modal_token.py
|
|
510
|
-
try:
|
|
511
|
-
# Execute the fix_modal_token.py script
|
|
512
|
-
logger.info("Falling back to fix_modal_token.py in thread...")
|
|
513
|
-
result = subprocess.run(
|
|
514
|
-
["python", os.path.join(os.path.dirname(__file__), "fix_modal_token.py")],
|
|
515
|
-
capture_output=True,
|
|
516
|
-
text=True
|
|
517
|
-
)
|
|
518
|
-
|
|
519
|
-
# Log the output
|
|
520
|
-
for line in result.stdout.splitlines():
|
|
521
|
-
logger.info(f"fix_modal_token.py (thread): {line}")
|
|
522
|
-
|
|
523
|
-
if result.returncode != 0:
|
|
524
|
-
logger.warning(f"fix_modal_token.py exited with code {result.returncode} in thread")
|
|
525
|
-
if result.stderr:
|
|
526
|
-
logger.error(f"fix_modal_token.py error in thread: {result.stderr}")
|
|
527
|
-
else:
|
|
528
|
-
logger.info("Modal token setup completed via fix_modal_token.py in thread")
|
|
529
|
-
except Exception as token_e:
|
|
530
|
-
logger.error(f"Error running fix_modal_token.py in thread: {token_e}")
|
|
531
|
-
|
|
532
|
-
# Explicitly print token status for debugging
|
|
533
|
-
logger.info(f"MODAL_TOKEN_ID in thread env: {os.environ.get('MODAL_TOKEN_ID')}")
|
|
534
|
-
logger.info(f"MODAL_TOKEN in thread env: {os.environ.get('MODAL_TOKEN')}")
|
|
535
|
-
|
|
536
|
-
result = create_modal_ssh_container(
|
|
537
|
-
gpu_type,
|
|
538
|
-
repo_url=repo_url,
|
|
539
|
-
repo_name=repo_name,
|
|
540
|
-
setup_commands=setup_commands,
|
|
541
|
-
volume_name=volume_name,
|
|
542
|
-
timeout_minutes=timeout_minutes,
|
|
543
|
-
ssh_password=ssh_password
|
|
544
|
-
)
|
|
545
|
-
|
|
546
|
-
# Clean up Modal token after container is created
|
|
547
|
-
cleanup_modal_token()
|
|
548
|
-
|
|
549
|
-
if result:
|
|
550
|
-
active_containers[container_id] = {
|
|
551
|
-
"container_id": result.get("app_name"),
|
|
552
|
-
"ssh_password": ssh_password,
|
|
553
|
-
"created_at": time.time(),
|
|
554
|
-
"type": "ssh"
|
|
555
|
-
}
|
|
556
|
-
logger.info(f"SSH container created successfully: {result.get('app_name')}")
|
|
557
|
-
else:
|
|
558
|
-
logger.error("Failed to create SSH container")
|
|
559
|
-
except Exception as e:
|
|
560
|
-
logger.error(f"Error in SSH container creation thread: {e}")
|
|
561
|
-
|
|
562
|
-
thread = threading.Thread(target=create_container_thread)
|
|
563
|
-
thread.daemon = True
|
|
564
|
-
thread.start()
|
|
565
|
-
|
|
566
|
-
return jsonify({
|
|
567
|
-
"message": "SSH container creation started",
|
|
568
|
-
"container_id": container_id,
|
|
569
|
-
"ssh_password": ssh_password
|
|
570
|
-
})
|
|
571
|
-
|
|
572
|
-
except Exception as e:
|
|
573
|
-
logger.error(f"Error creating SSH container: {e}")
|
|
574
|
-
return jsonify({"error": str(e)}), 500
|
|
575
|
-
|
|
576
|
-
@app.route('/api/container-status/<container_id>', methods=['GET'])
|
|
577
|
-
def container_status(container_id):
|
|
578
|
-
"""Get status of a container"""
|
|
579
|
-
if not authenticate_request():
|
|
580
|
-
return jsonify({"error": "Unauthorized"}), 401
|
|
581
|
-
|
|
582
|
-
if container_id in active_containers:
|
|
583
|
-
# Remove sensitive information like passwords
|
|
584
|
-
container_info = active_containers[container_id].copy()
|
|
585
|
-
if "ssh_password" in container_info:
|
|
586
|
-
del container_info["ssh_password"]
|
|
587
|
-
|
|
588
|
-
return jsonify({
|
|
589
|
-
"status": "active",
|
|
590
|
-
"info": container_info
|
|
591
|
-
})
|
|
592
|
-
else:
|
|
593
|
-
return jsonify({
|
|
594
|
-
"status": "not_found",
|
|
595
|
-
"message": "Container not found or has been terminated"
|
|
596
|
-
}), 404
|
|
597
|
-
|
|
598
|
-
@app.route('/api/terminate-container', methods=['POST'])
|
|
599
|
-
def terminate_container():
|
|
600
|
-
"""Terminate a Modal container"""
|
|
601
|
-
if not authenticate_request():
|
|
602
|
-
return jsonify({"error": "Unauthorized"}), 401
|
|
603
|
-
|
|
604
|
-
if not setup_modal_auth():
|
|
605
|
-
return jsonify({"error": "Failed to set up Modal authentication"}), 500
|
|
606
|
-
|
|
607
|
-
try:
|
|
608
|
-
data = request.json
|
|
609
|
-
container_id = data.get('container_id')
|
|
610
|
-
|
|
611
|
-
if not container_id:
|
|
612
|
-
return jsonify({"error": "Container ID is required"}), 400
|
|
613
|
-
|
|
614
|
-
if container_id not in active_containers:
|
|
615
|
-
return jsonify({"error": "Container not found"}), 404
|
|
616
|
-
|
|
617
|
-
modal_container_id = active_containers[container_id].get("container_id")
|
|
618
|
-
|
|
619
|
-
# Terminate the container using Modal CLI
|
|
620
|
-
import subprocess
|
|
621
|
-
result = subprocess.run(
|
|
622
|
-
["modal", "container", "terminate", modal_container_id],
|
|
623
|
-
capture_output=True,
|
|
624
|
-
text=True
|
|
625
|
-
)
|
|
626
|
-
|
|
627
|
-
if result.returncode == 0:
|
|
628
|
-
# Remove from active containers
|
|
629
|
-
del active_containers[container_id]
|
|
630
|
-
logger.info(f"Container terminated successfully: {modal_container_id}")
|
|
631
|
-
return jsonify({"message": "Container terminated successfully"})
|
|
632
|
-
else:
|
|
633
|
-
logger.error(f"Failed to terminate container: {result.stderr}")
|
|
634
|
-
return jsonify({"error": f"Failed to terminate container: {result.stderr}"}), 500
|
|
635
|
-
|
|
636
|
-
except Exception as e:
|
|
637
|
-
logger.error(f"Error terminating container: {e}")
|
|
638
|
-
return jsonify({"error": str(e)}), 500
|
|
639
|
-
|
|
640
|
-
# Register signal handlers for cleanup on shutdown
|
|
641
|
-
def signal_handler(sig, frame):
|
|
642
|
-
"""Handle signals for graceful shutdown"""
|
|
643
|
-
logger.info(f"Received signal {sig}, cleaning up and shutting down...")
|
|
644
|
-
cleanup_modal_token()
|
|
645
|
-
sys.exit(0)
|
|
646
|
-
|
|
647
|
-
# Register signal handlers for common termination signals
|
|
648
|
-
signal.signal(signal.SIGINT, signal_handler) # Ctrl+C
|
|
649
|
-
signal.signal(signal.SIGTERM, signal_handler) # Termination request
|
|
650
|
-
|
|
651
|
-
if __name__ == '__main__':
|
|
652
|
-
# Check if Modal token is set
|
|
653
|
-
if not MODAL_TOKEN:
|
|
654
|
-
logger.error("MODAL_TOKEN environment variable must be set!")
|
|
655
|
-
exit(1)
|
|
656
|
-
|
|
657
|
-
# Generate an admin key if not set
|
|
658
|
-
if not os.environ.get("ADMIN_KEY"):
|
|
659
|
-
admin_key = generate_api_key()
|
|
660
|
-
os.environ["ADMIN_KEY"] = admin_key
|
|
661
|
-
logger.info(f"Generated admin key: {admin_key}")
|
|
662
|
-
print(f"Admin key: {admin_key}")
|
|
663
|
-
|
|
664
|
-
port = int(os.environ.get("PORT", 5001)) # Default to 5001 to avoid macOS AirPlay conflict
|
|
665
|
-
app.run(host='0.0.0.0', port=port)
|