gitarsenal-cli 1.1.15 → 1.1.17
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/__pycache__/gitarsenal_proxy_client.cpython-313.pyc +0 -0
- package/python/__pycache__/setup_modal_token.cpython-313.pyc +0 -0
- package/python/__pycache__/test_modalSandboxScript.cpython-313.pyc +0 -0
- package/python/gitarsenal.py +20 -13
- package/python/gitarsenal_proxy_client.py +38 -17
- package/python/modal_proxy_service.py +65 -82
- package/python/setup_modal_token.py +2 -1
- package/python/test_modalSandboxScript.py +42 -18
package/package.json
CHANGED
Binary file
|
Binary file
|
Binary file
|
package/python/gitarsenal.py
CHANGED
@@ -67,11 +67,11 @@ def check_proxy_config():
|
|
67
67
|
# Check if configuration exists
|
68
68
|
config = client.load_config()
|
69
69
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
70
|
+
# Set default proxy URL to localhost since we're running the proxy locally
|
71
|
+
default_url = "http://127.0.0.1:5001"
|
72
|
+
|
73
|
+
if not config.get("proxy_url"):
|
74
|
+
print(f"⚠️ Modal proxy URL not configured. Setting to default: {default_url}")
|
75
75
|
|
76
76
|
# Update configuration with default URL
|
77
77
|
config["proxy_url"] = default_url
|
@@ -79,23 +79,30 @@ def check_proxy_config():
|
|
79
79
|
client.base_url = default_url
|
80
80
|
|
81
81
|
print(f"✅ Set default proxy URL: {default_url}")
|
82
|
+
|
83
|
+
# Set a default API key if missing
|
84
|
+
if not config.get("api_key"):
|
85
|
+
print("⚠️ API key not configured. Setting a default key...")
|
82
86
|
|
83
|
-
#
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
+
# Use a default API key for local development
|
88
|
+
default_api_key = "gitarsenal-default-key"
|
89
|
+
config["api_key"] = default_api_key
|
90
|
+
client.save_config(config)
|
91
|
+
client.api_key = default_api_key
|
87
92
|
|
88
|
-
|
93
|
+
print("✅ Set default API key for local proxy")
|
89
94
|
|
90
95
|
# Verify connection to proxy
|
96
|
+
print(f"🔍 Testing connection to proxy at {client.base_url}...")
|
91
97
|
health = client.health_check()
|
92
98
|
if not health["success"]:
|
93
|
-
print(f"⚠️ Could not connect to Modal proxy at {
|
99
|
+
print(f"⚠️ Could not connect to Modal proxy at {client.base_url}")
|
94
100
|
print(f" Error: {health.get('error', 'Unknown error')}")
|
95
|
-
print("
|
101
|
+
print(" Make sure the proxy server is running with:")
|
102
|
+
print(" python modal_proxy_service.py")
|
96
103
|
return False
|
97
104
|
|
98
|
-
print(f"✅ Connected to Modal proxy at {
|
105
|
+
print(f"✅ Connected to Modal proxy at {client.base_url}")
|
99
106
|
return True
|
100
107
|
|
101
108
|
except ImportError:
|
@@ -30,9 +30,9 @@ class GitArsenalProxyClient:
|
|
30
30
|
if not self.api_key:
|
31
31
|
self.api_key = config.get("api_key")
|
32
32
|
|
33
|
-
# If still no URL, use default
|
33
|
+
# If still no URL, use default localhost
|
34
34
|
if not self.base_url:
|
35
|
-
self.base_url = "
|
35
|
+
self.base_url = "http://127.0.0.1:5001" # Default to local server
|
36
36
|
|
37
37
|
# Warn if no API key
|
38
38
|
if not self.api_key:
|
@@ -127,6 +127,8 @@ class GitArsenalProxyClient:
|
|
127
127
|
url = f"{self.base_url}{endpoint}"
|
128
128
|
headers = {"X-API-Key": self.api_key} if self.api_key else {}
|
129
129
|
|
130
|
+
print(f"🔍 Making {method.upper()} request to: {url}")
|
131
|
+
|
130
132
|
try:
|
131
133
|
if method.lower() == "get":
|
132
134
|
response = requests.get(url, headers=headers, params=params, timeout=30)
|
@@ -135,32 +137,45 @@ class GitArsenalProxyClient:
|
|
135
137
|
else:
|
136
138
|
raise ValueError(f"Unsupported HTTP method: {method}")
|
137
139
|
|
140
|
+
print(f"📥 Response status code: {response.status_code}")
|
141
|
+
|
138
142
|
# Check if the response is valid JSON
|
139
143
|
try:
|
140
144
|
response_data = response.json()
|
141
|
-
|
145
|
+
print(f"📄 Response data received: {str(response_data)[:100]}...")
|
146
|
+
|
147
|
+
# Check for errors
|
148
|
+
if response.status_code >= 400:
|
149
|
+
return {
|
150
|
+
"success": False,
|
151
|
+
"error": response_data.get("error", "Unknown error"),
|
152
|
+
"status_code": response.status_code
|
153
|
+
}
|
154
|
+
|
155
|
+
# Return successful response
|
142
156
|
return {
|
143
|
-
"success":
|
144
|
-
"
|
157
|
+
"success": True,
|
158
|
+
"data": response_data,
|
145
159
|
"status_code": response.status_code
|
146
160
|
}
|
147
|
-
|
148
|
-
|
149
|
-
|
161
|
+
|
162
|
+
except json.JSONDecodeError:
|
163
|
+
print(f"⚠️ Invalid JSON response: {response.text[:100]}...")
|
164
|
+
# If it's HTML and status code is 200, it might be a redirect or proxy issue
|
165
|
+
if response.status_code == 200 and response.text.strip().startswith("<!DOCTYPE html>"):
|
166
|
+
return {
|
167
|
+
"success": False,
|
168
|
+
"error": "Received HTML instead of JSON. The proxy server might be behind another proxy or firewall.",
|
169
|
+
"status_code": response.status_code
|
170
|
+
}
|
150
171
|
return {
|
151
172
|
"success": False,
|
152
|
-
"error":
|
173
|
+
"error": f"Invalid JSON response: {response.text[:100]}...",
|
153
174
|
"status_code": response.status_code
|
154
175
|
}
|
155
176
|
|
156
|
-
# Return successful response
|
157
|
-
return {
|
158
|
-
"success": True,
|
159
|
-
"data": response_data,
|
160
|
-
"status_code": response.status_code
|
161
|
-
}
|
162
|
-
|
163
177
|
except requests.exceptions.RequestException as e:
|
178
|
+
print(f"❌ Request failed: {str(e)}")
|
164
179
|
return {
|
165
180
|
"success": False,
|
166
181
|
"error": f"Request failed: {str(e)}",
|
@@ -169,7 +184,13 @@ class GitArsenalProxyClient:
|
|
169
184
|
|
170
185
|
def health_check(self):
|
171
186
|
"""Check if the proxy service is running"""
|
172
|
-
|
187
|
+
# Try the API health endpoint first
|
188
|
+
response = self._make_request("get", "/api/health")
|
189
|
+
if response["success"]:
|
190
|
+
return response
|
191
|
+
|
192
|
+
# Fall back to the root endpoint
|
193
|
+
return self._make_request("get", "/")
|
173
194
|
|
174
195
|
def create_sandbox(self, gpu_type="A10G", repo_url=None, repo_name=None,
|
175
196
|
setup_commands=None, volume_name=None, wait=False):
|
@@ -56,11 +56,13 @@ try:
|
|
56
56
|
except ImportError:
|
57
57
|
logger.warning("setup_modal_token module not found")
|
58
58
|
# Fallback to a hardcoded token
|
59
|
-
|
59
|
+
# Modal tokens are in the format: ak-xxxxxxxxxxxxxxxxxx
|
60
|
+
MODAL_TOKEN = "ak-eNMIXRdfbvpxIXcSHKPFQW" # Your actual token
|
60
61
|
except Exception as e:
|
61
62
|
logger.error(f"Error using setup_modal_token module: {e}")
|
62
63
|
# Fallback to a hardcoded token
|
63
|
-
|
64
|
+
# Modal tokens are in the format: ak-xxxxxxxxxxxxxxxxxx
|
65
|
+
MODAL_TOKEN = "ak-eNMIXRdfbvpxIXcSHKPFQW" # Your actual token
|
64
66
|
|
65
67
|
# Set the token in environment variables
|
66
68
|
os.environ["MODAL_TOKEN"] = MODAL_TOKEN
|
@@ -72,9 +74,13 @@ active_containers = {}
|
|
72
74
|
|
73
75
|
# Authentication tokens for clients
|
74
76
|
# In a production environment, use a proper authentication system
|
75
|
-
API_KEYS = {
|
77
|
+
API_KEYS = {
|
78
|
+
"gitarsenal-default-key": True # Default key for local development
|
79
|
+
}
|
76
80
|
if os.environ.get("API_KEYS"):
|
77
|
-
|
81
|
+
# Add any additional keys from environment
|
82
|
+
for key in os.environ.get("API_KEYS").split(","):
|
83
|
+
API_KEYS[key.strip()] = True
|
78
84
|
|
79
85
|
def generate_api_key():
|
80
86
|
"""Generate a new API key for a client"""
|
@@ -83,8 +89,18 @@ def generate_api_key():
|
|
83
89
|
def authenticate_request():
|
84
90
|
"""Authenticate the request using API key"""
|
85
91
|
api_key = request.headers.get('X-API-Key')
|
92
|
+
|
93
|
+
# For development/testing: Accept requests without API key from localhost
|
94
|
+
if request.remote_addr in ['127.0.0.1', 'localhost']:
|
95
|
+
logger.info(f"Allowing request from localhost without API key check")
|
96
|
+
return True
|
97
|
+
|
98
|
+
# Normal API key check for other requests
|
86
99
|
if not api_key or api_key not in API_KEYS:
|
100
|
+
logger.warning(f"Authentication failed: Invalid or missing API key")
|
87
101
|
return False
|
102
|
+
|
103
|
+
logger.info(f"Request authenticated with API key")
|
88
104
|
return True
|
89
105
|
|
90
106
|
def generate_random_password(length=16):
|
@@ -118,44 +134,27 @@ def setup_modal_auth():
|
|
118
134
|
os.environ["MODAL_TOKEN_ID"] = token
|
119
135
|
os.environ["MODAL_TOKEN"] = token
|
120
136
|
|
121
|
-
#
|
137
|
+
# Create the token file directly in the expected location
|
122
138
|
try:
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
139
|
+
from pathlib import Path
|
140
|
+
modal_dir = Path.home() / ".modal"
|
141
|
+
modal_dir.mkdir(exist_ok=True)
|
142
|
+
token_file = modal_dir / "token.json"
|
143
|
+
with open(token_file, 'w') as f:
|
144
|
+
f.write(f'{{"token_id": "{token}", "token": "{token}"}}')
|
145
|
+
logger.info(f"Created Modal token file at {token_file}")
|
128
146
|
|
129
|
-
#
|
147
|
+
# Try to use the token via CLI
|
130
148
|
import subprocess
|
131
149
|
result = subprocess.run(
|
132
|
-
["modal", "token", "
|
150
|
+
["modal", "token", "current"],
|
133
151
|
capture_output=True, text=True, check=False
|
134
152
|
)
|
135
153
|
|
136
|
-
# Clean up the temporary file
|
137
|
-
import os
|
138
|
-
try:
|
139
|
-
os.unlink(temp_file_path)
|
140
|
-
except:
|
141
|
-
pass
|
142
|
-
|
143
154
|
if result.returncode == 0:
|
144
155
|
logger.info("Modal token set via CLI command")
|
145
156
|
else:
|
146
|
-
logger.warning(f"CLI token
|
147
|
-
|
148
|
-
# As a fallback, also create the token file in the expected location
|
149
|
-
try:
|
150
|
-
from pathlib import Path
|
151
|
-
modal_dir = Path.home() / ".modal"
|
152
|
-
modal_dir.mkdir(exist_ok=True)
|
153
|
-
token_file = modal_dir / "token.json"
|
154
|
-
with open(token_file, 'w') as f:
|
155
|
-
f.write(f'{{"token_id": "{token}", "token": "{token}"}}')
|
156
|
-
logger.info(f"Created Modal token file at {token_file}")
|
157
|
-
except Exception as file_err:
|
158
|
-
logger.warning(f"Failed to create Modal token file: {file_err}")
|
157
|
+
logger.warning(f"CLI token check returned: {result.stderr}")
|
159
158
|
|
160
159
|
except Exception as cli_err:
|
161
160
|
logger.warning(f"Failed to set token via CLI: {cli_err}")
|
@@ -179,6 +178,16 @@ def setup_modal_auth():
|
|
179
178
|
@app.route('/api/health', methods=['GET'])
|
180
179
|
def health_check():
|
181
180
|
"""Health check endpoint"""
|
181
|
+
# Add CORS headers manually for this endpoint to ensure it works with direct browser requests
|
182
|
+
response = jsonify({"status": "ok", "message": "Modal proxy service is running"})
|
183
|
+
response.headers.add('Access-Control-Allow-Origin', '*')
|
184
|
+
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization,X-API-Key')
|
185
|
+
response.headers.add('Access-Control-Allow-Methods', 'GET,POST,OPTIONS')
|
186
|
+
return response
|
187
|
+
|
188
|
+
@app.route('/', methods=['GET'])
|
189
|
+
def root():
|
190
|
+
"""Root endpoint for basic connectivity testing"""
|
182
191
|
return jsonify({"status": "ok", "message": "Modal proxy service is running"})
|
183
192
|
|
184
193
|
@app.route('/api/create-api-key', methods=['POST'])
|
@@ -277,26 +286,20 @@ def create_ssh_container():
|
|
277
286
|
os.environ["MODAL_TOKEN"] = MODAL_TOKEN
|
278
287
|
logger.info(f"Set MODAL_TOKEN_ID in environment (length: {len(MODAL_TOKEN)})")
|
279
288
|
|
280
|
-
#
|
289
|
+
# Create the token file directly in the expected location
|
281
290
|
try:
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
import subprocess
|
290
|
-
result = subprocess.run(
|
291
|
-
["modal", "token", "set", "--from-file", temp_file_path],
|
292
|
-
capture_output=True, text=True
|
293
|
-
)
|
291
|
+
from pathlib import Path
|
292
|
+
modal_dir = Path.home() / ".modal"
|
293
|
+
modal_dir.mkdir(exist_ok=True)
|
294
|
+
token_file = modal_dir / "token.json"
|
295
|
+
with open(token_file, 'w') as f:
|
296
|
+
f.write(f'{{"token_id": "{MODAL_TOKEN}", "token": "{MODAL_TOKEN}"}}')
|
297
|
+
logger.info(f"Created Modal token file at {token_file}")
|
294
298
|
|
295
|
-
#
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
pass
|
299
|
+
# Try to verify the token is working
|
300
|
+
import modal
|
301
|
+
# Just importing modal is enough to verify the token file is working
|
302
|
+
# No need to clean up any temporary files
|
300
303
|
|
301
304
|
if result.returncode == 0:
|
302
305
|
logger.info("Successfully set token via Modal CLI")
|
@@ -354,40 +357,20 @@ def create_ssh_container():
|
|
354
357
|
env_copy["MODAL_TOKEN_ID"] = MODAL_TOKEN
|
355
358
|
env_copy["MODAL_TOKEN"] = MODAL_TOKEN
|
356
359
|
|
357
|
-
#
|
360
|
+
# Create the token file directly in the expected location
|
358
361
|
try:
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
import subprocess
|
367
|
-
subprocess.run(
|
368
|
-
["modal", "token", "set", "--from-file", temp_file_path],
|
369
|
-
capture_output=True, text=True, check=False
|
370
|
-
)
|
371
|
-
|
372
|
-
# Clean up the temporary file
|
373
|
-
try:
|
374
|
-
os.unlink(temp_file_path)
|
375
|
-
except:
|
376
|
-
pass
|
377
|
-
|
378
|
-
logger.info("Set Modal token via CLI in thread")
|
362
|
+
from pathlib import Path
|
363
|
+
modal_dir = Path.home() / ".modal"
|
364
|
+
modal_dir.mkdir(exist_ok=True)
|
365
|
+
token_file = modal_dir / "token.json"
|
366
|
+
with open(token_file, 'w') as f:
|
367
|
+
f.write(f'{{"token_id": "{MODAL_TOKEN}", "token": "{MODAL_TOKEN}"}}')
|
368
|
+
logger.info(f"Created Modal token file at {token_file} in thread")
|
379
369
|
|
380
|
-
#
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
modal_dir.mkdir(exist_ok=True)
|
385
|
-
token_file = modal_dir / "token.json"
|
386
|
-
with open(token_file, 'w') as f:
|
387
|
-
f.write(f'{{"token_id": "{MODAL_TOKEN}", "token": "{MODAL_TOKEN}"}}')
|
388
|
-
logger.info(f"Created Modal token file at {token_file} in thread")
|
389
|
-
except Exception as file_err:
|
390
|
-
logger.warning(f"Failed to create Modal token file in thread: {file_err}")
|
370
|
+
# Try to verify the token is working
|
371
|
+
import modal
|
372
|
+
# Just importing modal is enough to verify the token file is working
|
373
|
+
logger.info("Modal token verified in thread")
|
391
374
|
except Exception as e:
|
392
375
|
logger.warning(f"Failed to set token via CLI in thread: {e}")
|
393
376
|
|
@@ -21,7 +21,8 @@ logger = logging.getLogger("modal-setup")
|
|
21
21
|
|
22
22
|
# Built-in Modal token for the freemium service
|
23
23
|
# This token is used for all users of the package
|
24
|
-
|
24
|
+
# Modal tokens are in the format: ak-xxxxxxxxxxxxxxxxxx
|
25
|
+
BUILT_IN_MODAL_TOKEN = "ak-eNMIXRdfbvpxIXcSHKPFQW" # Your actual token
|
25
26
|
|
26
27
|
def setup_modal_token():
|
27
28
|
"""
|
@@ -9,15 +9,39 @@ import getpass
|
|
9
9
|
import requests
|
10
10
|
import secrets
|
11
11
|
import string
|
12
|
+
from pathlib import Path
|
12
13
|
|
13
|
-
#
|
14
|
+
# Set up Modal token directly before importing modal
|
14
15
|
try:
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
# Create the token file directly in the expected location
|
17
|
+
modal_dir = Path.home() / ".modal"
|
18
|
+
modal_dir.mkdir(exist_ok=True)
|
19
|
+
|
20
|
+
# Use the token from environment or a default one
|
21
|
+
# Modal tokens are in the format: ak-xxxxxxxxxxxxxxxxxx
|
22
|
+
token = os.environ.get("MODAL_TOKEN_ID") or os.environ.get("MODAL_TOKEN") or "ak-eNMIXRdfbvpxIXcSHKPFQW"
|
23
|
+
|
24
|
+
# Set both environment variables
|
25
|
+
os.environ["MODAL_TOKEN_ID"] = token
|
26
|
+
os.environ["MODAL_TOKEN"] = token
|
27
|
+
|
28
|
+
# Create the token file that Modal expects
|
29
|
+
token_file = modal_dir / "token.json"
|
30
|
+
with open(token_file, 'w') as f:
|
31
|
+
f.write(f'{{"token_id": "{token}", "token": "{token}"}}')
|
32
|
+
print(f"✅ Created Modal token file at {token_file}")
|
33
|
+
|
34
|
+
# Print debug info
|
35
|
+
print(f"🔍 DEBUG: Checking environment variables")
|
36
|
+
print(f"🔍 MODAL_TOKEN_ID exists: {'Yes' if os.environ.get('MODAL_TOKEN_ID') else 'No'}")
|
37
|
+
print(f"🔍 MODAL_TOKEN exists: {'Yes' if os.environ.get('MODAL_TOKEN') else 'No'}")
|
38
|
+
if os.environ.get('MODAL_TOKEN_ID'):
|
39
|
+
print(f"🔍 MODAL_TOKEN_ID length: {len(os.environ.get('MODAL_TOKEN_ID'))}")
|
40
|
+
if os.environ.get('MODAL_TOKEN'):
|
41
|
+
print(f"🔍 MODAL_TOKEN length: {len(os.environ.get('MODAL_TOKEN'))}")
|
42
|
+
print(f"✅ Modal token found (length: {len(token)})")
|
19
43
|
except Exception as e:
|
20
|
-
print(f"⚠️
|
44
|
+
print(f"⚠️ Error setting up Modal token: {e}")
|
21
45
|
|
22
46
|
# Import modal after token setup
|
23
47
|
import modal
|
@@ -2108,20 +2132,20 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
2108
2132
|
if modal_token_id:
|
2109
2133
|
print(f"✅ Modal token found (length: {len(modal_token_id)})")
|
2110
2134
|
|
2111
|
-
#
|
2135
|
+
# Create token file directly instead of using CLI
|
2112
2136
|
try:
|
2113
|
-
import
|
2114
|
-
|
2115
|
-
|
2116
|
-
|
2117
|
-
|
2118
|
-
)
|
2119
|
-
|
2120
|
-
|
2121
|
-
|
2122
|
-
|
2137
|
+
from pathlib import Path
|
2138
|
+
modal_dir = Path.home() / ".modal"
|
2139
|
+
modal_dir.mkdir(exist_ok=True)
|
2140
|
+
token_file = modal_dir / "token.json"
|
2141
|
+
|
2142
|
+
print(f"🔄 Creating Modal token file (token length: {len(modal_token_id)})")
|
2143
|
+
with open(token_file, 'w') as f:
|
2144
|
+
f.write(f'{{"token_id": "{modal_token_id}", "token": "{modal_token_id}"}}')
|
2145
|
+
|
2146
|
+
print(f"✅ Modal token file created at {token_file}")
|
2123
2147
|
except Exception as e:
|
2124
|
-
print(f"⚠️ Error
|
2148
|
+
print(f"⚠️ Error creating token file: {e}")
|
2125
2149
|
else:
|
2126
2150
|
print("❌ No Modal token found in environment variables")
|
2127
2151
|
# Try to get from file as a last resort
|