gitarsenal-cli 1.1.3 → 1.1.4

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.4",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,126 @@
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
+ - Restart the proxy service after updating the token
98
+
99
+ 2. **Check that the proxy service is running**:
100
+ - Use `./gitarsenal.py proxy status` to check connectivity
101
+ - Verify the ngrok tunnel is active and the URL is correct
102
+
103
+ 3. **Check API key authentication**:
104
+ - Make sure your client is configured with a valid API key
105
+ - If needed, reconfigure with `./gitarsenal.py proxy configure`
106
+
107
+ 4. **Check server logs**:
108
+ - Look at `modal_proxy.log` on the server for detailed error messages
109
+
110
+ ### "Unauthorized" Error
111
+
112
+ If you see an "Unauthorized" error:
113
+
114
+ 1. **When creating an API key**:
115
+ - Make sure you're including the correct admin key in the request header
116
+ - Example: `-H "X-Admin-Key: your_admin_key_here"`
117
+
118
+ 2. **When using the API**:
119
+ - Make sure your client is configured with a valid API key
120
+ - Reconfigure with `./gitarsenal.py proxy configure`
121
+
122
+ ## Security Considerations
123
+
124
+ - Keep your admin key and API keys secure
125
+ - Use HTTPS if exposing the service publicly (ngrok provides this automatically)
126
+ - 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,9 @@ 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
+ logger.info("Modal token set in environment and config")
86
88
  return True
87
89
  except Exception as e:
88
90
  logger.error(f"Error setting up Modal authentication: {e}")
@@ -133,6 +135,11 @@ def create_sandbox():
133
135
  # Start sandbox creation in a separate thread
134
136
  def create_sandbox_thread():
135
137
  try:
138
+ # Ensure Modal token is set before creating sandbox
139
+ if not setup_modal_auth():
140
+ logger.error("Failed to set up Modal authentication in thread")
141
+ return
142
+
136
143
  result = create_modal_sandbox(
137
144
  gpu_type,
138
145
  repo_url=repo_url,
@@ -199,6 +206,18 @@ def create_ssh_container():
199
206
  # Start container creation in a separate thread
200
207
  def create_container_thread():
201
208
  try:
209
+ # Ensure Modal token is set before creating container
210
+ if not setup_modal_auth():
211
+ logger.error("Failed to set up Modal authentication in thread")
212
+ return
213
+
214
+ # Explicitly set the Modal token in the environment again for this thread
215
+ os.environ["MODAL_TOKEN_ID"] = MODAL_TOKEN
216
+
217
+ # Log token status for debugging
218
+ token_status = "Token is set" if MODAL_TOKEN else "Token is missing"
219
+ logger.info(f"Modal token status: {token_status}")
220
+
202
221
  result = create_modal_ssh_container(
203
222
  gpu_type,
204
223
  repo_url=repo_url,
@@ -2067,6 +2067,38 @@ 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
+ # This will raise an exception if not authenticated
2075
+ modal.config.get_current_workspace_name()
2076
+ print("✅ Modal authentication verified")
2077
+ except modal.exception.AuthError:
2078
+ print("\n" + "="*80)
2079
+ print("🔑 MODAL AUTHENTICATION REQUIRED")
2080
+ print("="*80)
2081
+ print("GitArsenal requires Modal authentication to create cloud environments.")
2082
+
2083
+ # Check if token is in environment
2084
+ modal_token = os.environ.get("MODAL_TOKEN_ID")
2085
+ if not modal_token:
2086
+ print("⚠️ No Modal token found in environment.")
2087
+ return None
2088
+ else:
2089
+ print("🔄 Using Modal token from environment")
2090
+ # Try to authenticate with the token
2091
+ try:
2092
+ # Set token directly in modal config
2093
+ modal.config._auth_config.token_id = modal_token
2094
+ print("✅ Modal token set in config")
2095
+ except Exception as e:
2096
+ print(f"⚠️ Error setting Modal token: {e}")
2097
+ return None
2098
+ except Exception as e:
2099
+ print(f"⚠️ Error checking Modal authentication: {e}")
2100
+ print("Continuing anyway, but Modal operations may fail")
2101
+
2070
2102
  # Generate a unique app name with timestamp to avoid conflicts
2071
2103
  timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
2072
2104
  app_name = f"ssh-container-{timestamp}"
@@ -2116,6 +2148,18 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2116
2148
  print(f"⚠️ Could not create default volume: {e}")
2117
2149
  print("⚠️ Continuing without persistent volume")
2118
2150
  volume = None
2151
+
2152
+ # Print debug info for authentication
2153
+ print("🔍 Modal authentication debug info:")
2154
+ modal_token = os.environ.get("MODAL_TOKEN_ID")
2155
+ print(f" - MODAL_TOKEN_ID in env: {'Yes' if modal_token else 'No'}")
2156
+ print(f" - Token length: {len(modal_token) if modal_token else 'N/A'}")
2157
+
2158
+ try:
2159
+ workspace = modal.config.get_current_workspace_name()
2160
+ print(f" - Current workspace: {workspace}")
2161
+ except Exception as e:
2162
+ print(f" - Error getting workspace: {e}")
2119
2163
 
2120
2164
  # Create SSH-enabled image
2121
2165
  ssh_image = (
@@ -2153,7 +2197,12 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2153
2197
  )
2154
2198
 
2155
2199
  # Create the Modal app
2156
- app = modal.App(app_name)
2200
+ try:
2201
+ app = modal.App(app_name)
2202
+ print("✅ Created Modal app successfully")
2203
+ except Exception as e:
2204
+ print(f"❌ Error creating Modal app: {e}")
2205
+ return None
2157
2206
 
2158
2207
  # Configure volumes if available
2159
2208
  volumes_config = {}
@@ -2319,6 +2368,10 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2319
2368
  "volume_name": volume_name,
2320
2369
  "volume_mount_path": volume_mount_path if volume else None
2321
2370
  }
2371
+ except modal.exception.AuthError as auth_err:
2372
+ print(f"❌ Modal authentication error: {auth_err}")
2373
+ print("🔑 Please check that your Modal token is valid and properly set")
2374
+ return None
2322
2375
  except KeyboardInterrupt:
2323
2376
  print("\n👋 Container startup interrupted")
2324
2377
  return None