gitarsenal-cli 1.1.3 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitarsenal-cli",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,145 @@
1
+ # Modal Proxy Service for GitArsenal CLI
2
+
3
+ This document explains how to set up and use the Modal proxy service with GitArsenal CLI.
4
+
5
+ ## What is the Modal Proxy Service?
6
+
7
+ The Modal proxy service allows users to access Modal services (like GPU-accelerated containers) without exposing the owner's Modal token. The service runs on a server and provides API endpoints for creating sandboxes and SSH containers, using API key authentication.
8
+
9
+ ## Server-Side Setup
10
+
11
+ 1. **Install Requirements**:
12
+ ```bash
13
+ pip install flask flask-cors modal python-dotenv
14
+ ```
15
+
16
+ 2. **Set Environment Variables**:
17
+ Create a `.env` file in the same directory as `modal_proxy_service.py`:
18
+ ```
19
+ MODAL_TOKEN=your_modal_token_here
20
+ ADMIN_KEY=your_admin_key_here
21
+ API_KEYS=optional_comma_separated_list_of_api_keys
22
+ ```
23
+
24
+ 3. **Start the Service**:
25
+ ```bash
26
+ python modal_proxy_service.py
27
+ ```
28
+
29
+ The service will start on port 5001 by default.
30
+
31
+ 4. **Create an API Key** (if not pre-configured in `.env`):
32
+ ```bash
33
+ curl -X POST http://localhost:5001/api/create-api-key \
34
+ -H "X-Admin-Key: your_admin_key_here"
35
+ ```
36
+
37
+ This will return a JSON response with a new API key:
38
+ ```json
39
+ {"api_key": "generated_api_key_here"}
40
+ ```
41
+
42
+ 5. **Optional: Use ngrok to expose the service publicly**:
43
+ ```bash
44
+ ngrok http 5001
45
+ ```
46
+
47
+ This will provide you with a public URL that you can share with users.
48
+
49
+ Current ngrok URL: `https://e74889c63199.ngrok-free.app`
50
+
51
+ ## Client-Side Setup
52
+
53
+ 1. **Configure the proxy client**:
54
+ ```bash
55
+ ./gitarsenal.py proxy configure
56
+ ```
57
+
58
+ You'll be prompted to enter:
59
+ - The proxy URL (the default is now set to `https://e74889c63199.ngrok-free.app`)
60
+ - Your API key
61
+
62
+ 2. **Check proxy service status**:
63
+ ```bash
64
+ ./gitarsenal.py proxy status
65
+ ```
66
+
67
+ ## Using the Proxy Service
68
+
69
+ ### Create an SSH Container
70
+
71
+ ```bash
72
+ # Using the proxy command
73
+ ./gitarsenal.py proxy ssh --gpu A10G --repo-url https://github.com/username/repo.git --wait
74
+
75
+ # Or using the standard ssh command with --use-proxy flag
76
+ ./gitarsenal.py ssh --use-proxy --gpu A10G --repo-url https://github.com/username/repo.git
77
+ ```
78
+
79
+ ### Create a Sandbox
80
+
81
+ ```bash
82
+ # Using the proxy command
83
+ ./gitarsenal.py proxy sandbox --gpu A10G --repo-url https://github.com/username/repo.git --wait
84
+
85
+ # Or using the standard sandbox command with --use-proxy flag
86
+ ./gitarsenal.py sandbox --use-proxy --gpu A10G --repo-url https://github.com/username/repo.git
87
+ ```
88
+
89
+ ## Troubleshooting
90
+
91
+ ### "Token missing" Error
92
+
93
+ If you see a "Token missing" error when creating containers through the proxy service:
94
+
95
+ 1. **Check that the MODAL_TOKEN is set on the server**:
96
+ - Verify the `.env` file has a valid MODAL_TOKEN
97
+ - Ensure the token is correctly formatted and not expired
98
+ - Restart the proxy service after updating the token
99
+
100
+ 2. **Manually set the token on the server**:
101
+ ```bash
102
+ # On the server running the proxy service
103
+ export MODAL_TOKEN_ID=your_modal_token_here
104
+ modal token set your_modal_token_here
105
+ ```
106
+
107
+ 3. **Verify the token works directly on the server**:
108
+ ```bash
109
+ # Test if the token works by listing Modal volumes
110
+ modal volume list
111
+ ```
112
+
113
+ 4. **Check the proxy service logs**:
114
+ - Look at `modal_proxy.log` on the server for detailed error messages
115
+ - Check for messages like "Modal token verification failed"
116
+ - Ensure the token is being properly passed to the Modal API
117
+
118
+ 5. **Restart the proxy service**:
119
+ ```bash
120
+ # Kill the existing process and restart
121
+ pkill -f modal_proxy_service.py
122
+ python modal_proxy_service.py
123
+ ```
124
+
125
+ 6. **Check ngrok connection**:
126
+ - Verify the ngrok tunnel is active and the URL is correct
127
+ - Ensure requests are being properly forwarded to the proxy service
128
+
129
+ ### "Unauthorized" Error
130
+
131
+ If you see an "Unauthorized" error:
132
+
133
+ 1. **When creating an API key**:
134
+ - Make sure you're including the correct admin key in the request header
135
+ - Example: `-H "X-Admin-Key: your_admin_key_here"`
136
+
137
+ 2. **When using the API**:
138
+ - Make sure your client is configured with a valid API key
139
+ - Reconfigure with `./gitarsenal.py proxy configure`
140
+
141
+ ## Security Considerations
142
+
143
+ - Keep your admin key and API keys secure
144
+ - Use HTTPS if exposing the service publicly (ngrok provides this automatically)
145
+ - Consider implementing additional authentication mechanisms for production use
@@ -60,21 +60,49 @@ def check_modal_auth():
60
60
  return False
61
61
 
62
62
  def check_proxy_config():
63
- """Check if Modal proxy is configured"""
63
+ """Check if Modal proxy configuration exists and is valid"""
64
64
  try:
65
- # Import the proxy client
66
65
  from gitarsenal_proxy_client import GitArsenalProxyClient
67
66
 
68
- # Create client and load config
67
+ # Initialize client
69
68
  client = GitArsenalProxyClient()
69
+
70
+ # Check if configuration exists
70
71
  config = client.load_config()
71
72
 
72
- # Check if proxy URL and API key are configured
73
- if "proxy_url" in config and "api_key" in config:
74
- return True
75
- else:
73
+ if not config.get("proxy_url") or not config.get("api_key"):
74
+ print("⚠️ Modal proxy not configured. Setting up with default values...")
75
+
76
+ # Set default proxy URL to the ngrok URL
77
+ default_url = "https://e74889c63199.ngrok-free.app"
78
+
79
+ # Update configuration with default URL
80
+ config["proxy_url"] = default_url
81
+ client.save_config(config)
82
+ client.base_url = default_url
83
+
84
+ print(f"✅ Set default proxy URL: {default_url}")
85
+
86
+ # If API key is missing, prompt for configuration
87
+ if not config.get("api_key"):
88
+ print("⚠️ API key is still missing. Please configure:")
89
+ client.configure(interactive=True)
90
+
76
91
  return False
92
+
93
+ # Verify connection to proxy
94
+ health = client.health_check()
95
+ if not health["success"]:
96
+ print(f"⚠️ Could not connect to Modal proxy at {config.get('proxy_url')}")
97
+ print(f" Error: {health.get('error', 'Unknown error')}")
98
+ print(" Run './gitarsenal.py proxy configure' to update configuration.")
99
+ return False
100
+
101
+ print(f"✅ Connected to Modal proxy at {config.get('proxy_url')}")
102
+ return True
103
+
77
104
  except ImportError:
105
+ print("⚠️ GitArsenalProxyClient module not found")
78
106
  return False
79
107
  except Exception as e:
80
108
  print(f"⚠️ Error checking proxy configuration: {e}")
@@ -384,7 +412,21 @@ def main():
384
412
  from gitarsenal_proxy_client import GitArsenalProxyClient
385
413
  client = GitArsenalProxyClient()
386
414
 
415
+ # Ensure proxy is configured
416
+ config = client.load_config()
417
+ if not config.get("proxy_url") or not config.get("api_key"):
418
+ print("\n⚠️ Modal proxy service is not fully configured.")
419
+ print("Running configuration wizard...")
420
+ client.configure(interactive=True)
421
+ # Reload config after configuration
422
+ config = client.load_config()
423
+ if not config.get("proxy_url") or not config.get("api_key"):
424
+ print("❌ Proxy configuration failed or was cancelled.")
425
+ return 1
426
+ print("✅ Proxy configuration completed successfully.")
427
+
387
428
  # Create SSH container through proxy
429
+ print(f"🚀 Creating SSH container through proxy service ({config.get('proxy_url')})")
388
430
  result = client.create_ssh_container(
389
431
  gpu_type=args.gpu,
390
432
  repo_url=args.repo_url,
@@ -475,6 +517,31 @@ def main():
475
517
 
476
518
  elif args.proxy_command == "ssh":
477
519
  # Create SSH container through proxy
520
+ print("🚀 Creating SSH container through proxy...")
521
+
522
+ # Verify proxy configuration is complete
523
+ config = client.load_config()
524
+ if not config.get("proxy_url") or not config.get("api_key"):
525
+ print("⚠️ Proxy configuration incomplete. Running configuration wizard...")
526
+ client.configure(interactive=True)
527
+ # Reload config after configuration
528
+ config = client.load_config()
529
+ if not config.get("proxy_url") or not config.get("api_key"):
530
+ print("❌ Proxy configuration failed or was cancelled.")
531
+ return 1
532
+ print("✅ Proxy configuration completed successfully.")
533
+
534
+ # Check proxy service health
535
+ health = client.health_check()
536
+ if not health["success"]:
537
+ print(f"❌ Could not connect to proxy service at {config.get('proxy_url')}")
538
+ print(f" Error: {health.get('error', 'Unknown error')}")
539
+ print(" Please check if the proxy service is running.")
540
+ return 1
541
+
542
+ print(f"✅ Connected to proxy service at {config.get('proxy_url')}")
543
+
544
+ # Create SSH container
478
545
  result = client.create_ssh_container(
479
546
  gpu_type=args.gpu,
480
547
  repo_url=args.repo_url,
@@ -489,8 +556,19 @@ def main():
489
556
  print("❌ Failed to create SSH container through proxy service")
490
557
  return 1
491
558
 
492
- print("✅ SSH container created successfully through proxy service")
493
-
559
+ # Print connection information if available
560
+ if isinstance(result, dict):
561
+ if "container_id" in result:
562
+ print(f"📋 Container ID: {result['container_id']}")
563
+ if "ssh_password" in result:
564
+ print(f"🔐 SSH Password: {result['ssh_password']}")
565
+
566
+ print("✅ SSH container creation process initiated through proxy service")
567
+
568
+ if not args.wait:
569
+ print("\n⚠️ Container creation is asynchronous. Use --wait flag to wait for it to be ready.")
570
+ print(" You can check the status later using the container ID above.")
571
+
494
572
  else:
495
573
  print(f"❌ Unknown proxy command: {args.proxy_command}")
496
574
  proxy_parser.print_help()
@@ -32,7 +32,7 @@ class GitArsenalProxyClient:
32
32
 
33
33
  # If still no URL, use default
34
34
  if not self.base_url:
35
- self.base_url = "http://localhost:5001" # Default to 5001 to avoid macOS AirPlay conflict
35
+ self.base_url = "https://e74889c63199.ngrok-free.app" # Default to ngrok URL
36
36
 
37
37
  # Warn if no API key
38
38
  if not self.api_key:
@@ -217,6 +217,28 @@ class GitArsenalProxyClient:
217
217
  def create_ssh_container(self, gpu_type="A10G", repo_url=None, repo_name=None,
218
218
  setup_commands=None, volume_name=None, timeout=60, wait=False):
219
219
  """Create a Modal SSH container through the proxy service"""
220
+ # Verify we have a valid API key
221
+ if not self.api_key:
222
+ print("❌ No API key provided. Please configure the proxy client first:")
223
+ print(" ./gitarsenal.py proxy configure")
224
+ return None
225
+
226
+ # Verify proxy URL is set
227
+ if not self.base_url:
228
+ print("❌ No proxy URL provided. Please configure the proxy client first:")
229
+ print(" ./gitarsenal.py proxy configure")
230
+ return None
231
+
232
+ # Check if proxy is reachable
233
+ health = self.health_check()
234
+ if not health["success"]:
235
+ print(f"❌ Could not connect to proxy service at {self.base_url}")
236
+ print(f" Error: {health.get('error', 'Unknown error')}")
237
+ print(" Please check if the proxy service is running and properly configured.")
238
+ return None
239
+
240
+ print(f"✅ Connected to proxy service at {self.base_url}")
241
+
220
242
  data = {
221
243
  "gpu_type": gpu_type,
222
244
  "repo_url": repo_url,
@@ -226,10 +248,21 @@ class GitArsenalProxyClient:
226
248
  "timeout": timeout
227
249
  }
228
250
 
251
+ print("🔄 Sending request to create SSH container...")
229
252
  response = self._make_request("post", "/api/create-ssh-container", data=data)
230
253
 
231
254
  if not response["success"]:
232
255
  print(f"❌ Failed to create SSH container: {response['error']}")
256
+ print(f" Status code: {response.get('status_code', 'Unknown')}")
257
+
258
+ # Additional error handling for common issues
259
+ if response.get('status_code') == 401:
260
+ print(" Authentication failed. Please check your API key.")
261
+ print(" Run './gitarsenal.py proxy configure' to set up a new API key.")
262
+ elif response.get('status_code') == 500:
263
+ print(" Server error. The proxy service might be misconfigured.")
264
+ print(" Check if the MODAL_TOKEN is properly set on the server.")
265
+
233
266
  return None
234
267
 
235
268
  container_id = response["data"].get("container_id")
@@ -253,21 +286,21 @@ class GitArsenalProxyClient:
253
286
  status_data = status_response["data"]
254
287
  if status_data.get("status") == "active":
255
288
  print(f"✅ SSH container is ready!")
256
- return {
257
- "container_id": container_id,
258
- "ssh_password": ssh_password,
259
- "info": status_data.get("info")
260
- }
289
+ container_info = status_data.get("info", {})
290
+
291
+ # Add the password back since it's removed in the status endpoint
292
+ container_info["ssh_password"] = ssh_password
293
+
294
+ return container_info
261
295
 
262
296
  print(".", end="", flush=True)
263
297
  time.sleep(poll_interval)
264
298
 
265
299
  print("\n⚠️ Timed out waiting for SSH container to be ready")
300
+ print("The container may still be initializing. Check status with:")
301
+ print(f"./gitarsenal.py proxy status {container_id}")
266
302
 
267
- return {
268
- "container_id": container_id,
269
- "ssh_password": ssh_password
270
- }
303
+ return {"container_id": container_id, "ssh_password": ssh_password}
271
304
 
272
305
  def get_container_status(self, container_id):
273
306
  """Get the status of a container"""
@@ -82,7 +82,30 @@ def setup_modal_auth():
82
82
  try:
83
83
  # Set the token in the environment
84
84
  os.environ["MODAL_TOKEN_ID"] = MODAL_TOKEN
85
- logger.info("Modal token set in environment")
85
+ # Also set the token directly in modal.config
86
+ modal.config._auth_config.token_id = MODAL_TOKEN
87
+
88
+ # Verify token is working by attempting a simple operation
89
+ try:
90
+ # Try to list volumes as a simple API test
91
+ test_vol = modal.Volume.list()
92
+ logger.info("Modal token verified successfully - able to list volumes")
93
+ except Exception as vol_err:
94
+ logger.error(f"Modal token verification failed: {vol_err}")
95
+ # Try setting token again with different approach
96
+ try:
97
+ import subprocess
98
+ result = subprocess.run(
99
+ ["modal", "token", "set", MODAL_TOKEN],
100
+ capture_output=True, text=True, check=True
101
+ )
102
+ logger.info("Modal token set via CLI command")
103
+ return True
104
+ except Exception as cli_err:
105
+ logger.error(f"Failed to set token via CLI: {cli_err}")
106
+ return False
107
+
108
+ logger.info("Modal token set in environment and config")
86
109
  return True
87
110
  except Exception as e:
88
111
  logger.error(f"Error setting up Modal authentication: {e}")
@@ -133,6 +156,11 @@ def create_sandbox():
133
156
  # Start sandbox creation in a separate thread
134
157
  def create_sandbox_thread():
135
158
  try:
159
+ # Ensure Modal token is set before creating sandbox
160
+ if not setup_modal_auth():
161
+ logger.error("Failed to set up Modal authentication in thread")
162
+ return
163
+
136
164
  result = create_modal_sandbox(
137
165
  gpu_type,
138
166
  repo_url=repo_url,
@@ -196,9 +224,45 @@ def create_ssh_container():
196
224
  # Create a unique ID for this container
197
225
  container_id = str(uuid.uuid4())
198
226
 
227
+ # Log token status for debugging
228
+ token_status = "Token is set" if MODAL_TOKEN else "Token is missing"
229
+ logger.info(f"Modal token status before thread: {token_status}")
230
+ logger.info(f"Modal token length: {len(MODAL_TOKEN) if MODAL_TOKEN else 'N/A'}")
231
+
232
+ # Ensure Modal token is set in environment
233
+ if MODAL_TOKEN:
234
+ os.environ["MODAL_TOKEN_ID"] = MODAL_TOKEN
235
+ # Also set directly in modal.config
236
+ modal.config._auth_config.token_id = MODAL_TOKEN
237
+ logger.info("Modal token explicitly set in environment and config")
238
+
199
239
  # Start container creation in a separate thread
200
240
  def create_container_thread():
201
241
  try:
242
+ # Ensure Modal token is set before creating container
243
+ if not setup_modal_auth():
244
+ logger.error("Failed to set up Modal authentication in thread")
245
+ return
246
+
247
+ # Explicitly set the Modal token in the environment again for this thread
248
+ os.environ["MODAL_TOKEN_ID"] = MODAL_TOKEN
249
+
250
+ # Directly set the token in Modal config
251
+ modal.config._auth_config.token_id = MODAL_TOKEN
252
+
253
+ # Log token status for debugging
254
+ token_status = "Token is set" if MODAL_TOKEN else "Token is missing"
255
+ logger.info(f"Modal token status in thread: {token_status}")
256
+ logger.info(f"Modal token length in thread: {len(MODAL_TOKEN) if MODAL_TOKEN else 'N/A'}")
257
+
258
+ # Verify token is actually working
259
+ try:
260
+ # Try a simple Modal operation to verify authentication
261
+ import modal.cli.app_create
262
+ logger.info("Modal module imported successfully in thread")
263
+ except Exception as auth_err:
264
+ logger.error(f"Modal authentication verification error: {auth_err}")
265
+
202
266
  result = create_modal_ssh_container(
203
267
  gpu_type,
204
268
  repo_url=repo_url,
@@ -2067,6 +2067,59 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2067
2067
  volume_name=None, timeout_minutes=60, ssh_password=None):
2068
2068
  """Create a Modal SSH container with GPU support and tunneling"""
2069
2069
 
2070
+ # Check if Modal is authenticated
2071
+ try:
2072
+ # Try to access Modal token to check authentication
2073
+ try:
2074
+ # Check if token is set in environment
2075
+ modal_token = os.environ.get("MODAL_TOKEN_ID")
2076
+ if not modal_token:
2077
+ print("⚠️ MODAL_TOKEN_ID not found in environment.")
2078
+ # Try to set from modal.config if available
2079
+ try:
2080
+ modal_token = modal.config._auth_config.token_id
2081
+ if modal_token:
2082
+ print("✅ Found token in modal.config")
2083
+ # Set it in environment for consistency
2084
+ os.environ["MODAL_TOKEN_ID"] = modal_token
2085
+ except Exception as e:
2086
+ print(f"⚠️ Error accessing modal.config: {e}")
2087
+
2088
+ # If still no token, try to get workspace name as authentication test
2089
+ if not modal_token:
2090
+ # This will raise an exception if not authenticated
2091
+ workspace = modal.config.get_current_workspace_name()
2092
+ print(f"✅ Modal authentication verified (workspace: {workspace})")
2093
+ else:
2094
+ print(f"✅ Modal token found (length: {len(modal_token)})")
2095
+ # Explicitly set the token in modal config
2096
+ modal.config._auth_config.token_id = modal_token
2097
+ print("✅ Token explicitly set in modal.config")
2098
+ except modal.exception.AuthError:
2099
+ print("\n" + "="*80)
2100
+ print("🔑 MODAL AUTHENTICATION REQUIRED")
2101
+ print("="*80)
2102
+ print("GitArsenal requires Modal authentication to create cloud environments.")
2103
+
2104
+ # Check if token is in environment
2105
+ modal_token = os.environ.get("MODAL_TOKEN_ID")
2106
+ if not modal_token:
2107
+ print("⚠️ No Modal token found in environment.")
2108
+ return None
2109
+ else:
2110
+ print("🔄 Using Modal token from environment")
2111
+ # Try to authenticate with the token
2112
+ try:
2113
+ # Set token directly in modal config
2114
+ modal.config._auth_config.token_id = modal_token
2115
+ print("✅ Modal token set in config")
2116
+ except Exception as e:
2117
+ print(f"⚠️ Error setting Modal token: {e}")
2118
+ return None
2119
+ except Exception as e:
2120
+ print(f"⚠️ Error checking Modal authentication: {e}")
2121
+ print("Continuing anyway, but Modal operations may fail")
2122
+
2070
2123
  # Generate a unique app name with timestamp to avoid conflicts
2071
2124
  timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
2072
2125
  app_name = f"ssh-container-{timestamp}"
@@ -2116,6 +2169,41 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2116
2169
  print(f"⚠️ Could not create default volume: {e}")
2117
2170
  print("⚠️ Continuing without persistent volume")
2118
2171
  volume = None
2172
+
2173
+ # Print debug info for authentication
2174
+ print("🔍 Modal authentication debug info:")
2175
+ modal_token = os.environ.get("MODAL_TOKEN_ID")
2176
+ print(f" - MODAL_TOKEN_ID in env: {'Yes' if modal_token else 'No'}")
2177
+ print(f" - Token length: {len(modal_token) if modal_token else 'N/A'}")
2178
+
2179
+ try:
2180
+ workspace = modal.config.get_current_workspace_name()
2181
+ print(f" - Current workspace: {workspace}")
2182
+ except Exception as e:
2183
+ print(f" - Error getting workspace: {e}")
2184
+
2185
+ # Verify token is valid by trying a simple Modal operation
2186
+ try:
2187
+ print("🔍 Verifying Modal token validity...")
2188
+ # Try to access Modal API
2189
+ if not modal_token:
2190
+ print("❌ No Modal token available. Cannot proceed.")
2191
+ return None
2192
+
2193
+ # Set token directly in modal config again to be sure
2194
+ modal.config._auth_config.token_id = modal_token
2195
+
2196
+ # Try to list volumes as a simple API test
2197
+ try:
2198
+ test_vol = modal.Volume.list()
2199
+ print(f"✅ Modal token is valid! Successfully listed volumes.")
2200
+ except Exception as vol_err:
2201
+ print(f"❌ Token validation failed: {vol_err}")
2202
+ print("Please check that your Modal token is valid and properly set.")
2203
+ return None
2204
+ except Exception as e:
2205
+ print(f"❌ Error during token validation: {e}")
2206
+ return None
2119
2207
 
2120
2208
  # Create SSH-enabled image
2121
2209
  ssh_image = (
@@ -2153,7 +2241,12 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2153
2241
  )
2154
2242
 
2155
2243
  # Create the Modal app
2156
- app = modal.App(app_name)
2244
+ try:
2245
+ app = modal.App(app_name)
2246
+ print("✅ Created Modal app successfully")
2247
+ except Exception as e:
2248
+ print(f"❌ Error creating Modal app: {e}")
2249
+ return None
2157
2250
 
2158
2251
  # Configure volumes if available
2159
2252
  volumes_config = {}
@@ -2319,6 +2412,10 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2319
2412
  "volume_name": volume_name,
2320
2413
  "volume_mount_path": volume_mount_path if volume else None
2321
2414
  }
2415
+ except modal.exception.AuthError as auth_err:
2416
+ print(f"❌ Modal authentication error: {auth_err}")
2417
+ print("🔑 Please check that your Modal token is valid and properly set")
2418
+ return None
2322
2419
  except KeyboardInterrupt:
2323
2420
  print("\n👋 Container startup interrupted")
2324
2421
  return None