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.
Files changed (34) hide show
  1. package/.venv_status.json +1 -1
  2. package/package.json +1 -1
  3. package/python/__pycache__/auth_manager.cpython-313.pyc +0 -0
  4. package/python/__pycache__/command_manager.cpython-313.pyc +0 -0
  5. package/python/__pycache__/fetch_modal_tokens.cpython-313.pyc +0 -0
  6. package/python/__pycache__/llm_debugging.cpython-313.pyc +0 -0
  7. package/python/__pycache__/modal_container.cpython-313.pyc +0 -0
  8. package/python/__pycache__/shell.cpython-313.pyc +0 -0
  9. package/python/api_integration.py +0 -0
  10. package/python/command_manager.py +613 -0
  11. package/python/credentials_manager.py +0 -0
  12. package/python/fetch_modal_tokens.py +0 -0
  13. package/python/fix_modal_token.py +0 -0
  14. package/python/fix_modal_token_advanced.py +0 -0
  15. package/python/gitarsenal.py +0 -0
  16. package/python/gitarsenal_proxy_client.py +0 -0
  17. package/python/llm_debugging.py +1369 -0
  18. package/python/modal_container.py +626 -0
  19. package/python/setup.py +15 -0
  20. package/python/setup_modal_token.py +0 -39
  21. package/python/shell.py +627 -0
  22. package/python/test_modalSandboxScript.py +75 -2639
  23. package/scripts/postinstall.js +22 -23
  24. package/python/__pycache__/credentials_manager.cpython-313.pyc +0 -0
  25. package/python/__pycache__/test_modalSandboxScript.cpython-313.pyc +0 -0
  26. package/python/__pycache__/test_modalSandboxScript_stable.cpython-313.pyc +0 -0
  27. package/python/debug_delete.py +0 -167
  28. package/python/documentation.py +0 -76
  29. package/python/fix_setup_commands.py +0 -116
  30. package/python/modal_auth_patch.py +0 -178
  31. package/python/modal_proxy_service.py +0 -665
  32. package/python/modal_token_solution.py +0 -293
  33. package/python/test_dynamic_commands.py +0 -147
  34. 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)