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 +1 -1
- package/python/MODAL_PROXY_README.md +126 -0
- package/python/__pycache__/gitarsenal_proxy_client.cpython-313.pyc +0 -0
- package/python/gitarsenal.py +87 -9
- package/python/gitarsenal_proxy_client.py +43 -10
- package/python/modal_proxy_service.py +20 -1
- package/python/test_modalSandboxScript.py +54 -1
package/package.json
CHANGED
@@ -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
|
Binary file
|
package/python/gitarsenal.py
CHANGED
@@ -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
|
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
|
-
#
|
67
|
+
# Initialize client
|
69
68
|
client = GitArsenalProxyClient()
|
69
|
+
|
70
|
+
# Check if configuration exists
|
70
71
|
config = client.load_config()
|
71
72
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
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 = "
|
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
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
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
|
-
|
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
|
-
|
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
|