gitarsenal-cli 1.2.2 → 1.2.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.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
package/python/README.md CHANGED
@@ -1,248 +1,69 @@
1
- # GitArsenal CLI
1
+ # GitArsenal CLI Python Modules
2
2
 
3
- GitArsenal CLI is a powerful tool for setting up and running GPU-accelerated environments in the cloud using Modal.
3
+ This directory contains Python modules for the GitArsenal CLI.
4
4
 
5
- ## Installation
5
+ ## Modal Integration
6
6
 
7
- ```bash
8
- # Clone the repository
9
- git clone https://github.com/yourusername/gitarsenal-cli.git
10
- cd gitarsenal-cli/python
11
-
12
- # Install dependencies
13
- pip install -r requirements.txt
14
- ```
7
+ The GitArsenal CLI integrates with Modal for creating sandboxes and SSH containers. The following modules are available:
15
8
 
16
- ## First-Time Setup
9
+ - `modal_proxy_service.py`: A proxy service for Modal operations
10
+ - `test_modalSandboxScript.py`: Creates Modal sandboxes and SSH containers
11
+ - `setup_modal_token.py`: Sets up the Modal token in the environment
12
+ - `fetch_modal_tokens.py`: Fetches Modal tokens from the proxy server
17
13
 
18
- GitArsenal CLI comes with a built-in Modal token for the freemium service, so you don't need to set up your own Modal credentials. Just install and start using it!
14
+ ## Security Features
19
15
 
20
- ### 1. Install Modal
16
+ ### Modal Token Cleanup
21
17
 
22
- ```bash
23
- pip install modal
24
- ```
18
+ For enhanced security, GitArsenal CLI now automatically cleans up Modal tokens after SSH containers are created. This ensures that your Modal token is not left in the environment or on disk after the container is started.
25
19
 
26
- ### 2. Optional: Set Up Additional Credentials
20
+ The cleanup process:
21
+ 1. Removes Modal token environment variables (MODAL_TOKEN_ID, MODAL_TOKEN, MODAL_TOKEN_SECRET)
22
+ 2. Deletes Modal token files (.modal/token.json, .modal/token_alt.json, .modalconfig)
23
+ 3. Invalidates Modal sessions by removing session files and directories
24
+ 4. Runs automatically after SSH container creation
25
+ 5. Runs on service shutdown via signal handlers
27
26
 
28
- If you want to use additional features, you can set up other credentials:
29
-
30
- ```bash
31
- # Set up optional credentials
32
- ./gitarsenal.py credentials setup
33
- ```
34
-
35
- This will guide you through setting up:
36
- - **OpenAI API Key**: Used for debugging failed commands (optional)
37
- - **Hugging Face Token**: Used for accessing Hugging Face models (optional)
38
- - **Weights & Biases API Key**: Used for experiment tracking (optional)
27
+ This prevents potential token leakage when containers are shared or when the service is stopped, and ensures that users cannot continue to use Modal CLI commands after the container is created.
39
28
 
40
29
  ## Usage
41
30
 
42
- ### Creating a Modal Sandbox
43
-
44
- ```bash
45
- ./gitarsenal.py sandbox --gpu A10G --repo-url "https://github.com/username/repo.git"
46
- ```
47
-
48
- ### Creating an SSH Container
49
-
50
- ```bash
51
- ./gitarsenal.py ssh --gpu A10G --repo-url "https://github.com/username/repo.git"
52
- ```
53
-
54
- ### Options
55
-
56
- - `--gpu`: GPU type (A10G, A100, H100, T4, V100)
57
- - `--repo-url`: Repository URL to clone
58
- - `--repo-name`: Repository name override
59
- - `--setup-commands`: Setup commands to run
60
- - `--volume-name`: Name of the Modal volume for persistent storage
61
- - `--timeout`: Container timeout in minutes (SSH mode only, default: 60)
62
- - `--use-proxy`: Use Modal proxy service instead of direct Modal access
63
-
64
- ## Using the Modal Proxy Service
65
-
66
- GitArsenal CLI now supports using a Modal proxy service, which allows you to use Modal services without having your own Modal token. This is useful for teams or organizations where a single Modal account is shared.
67
-
68
- ### Setting Up the Proxy Service
69
-
70
- #### 1. Create an Environment File
31
+ To use the GitArsenal CLI Python modules, you can import them directly or use the provided scripts.
71
32
 
72
- Copy the example environment file and edit it:
33
+ ### Creating a Modal SSH Container
73
34
 
74
- ```bash
75
- cp .env.example .env
76
- ```
35
+ ```python
36
+ from test_modalSandboxScript import create_modal_ssh_container
77
37
 
78
- Edit the `.env` file to add your Modal token:
38
+ result = create_modal_ssh_container(
39
+ gpu_type="A10G",
40
+ repo_url="https://github.com/user/repo",
41
+ repo_name="repo",
42
+ setup_commands=["pip install -r requirements.txt"],
43
+ timeout_minutes=60
44
+ )
79
45
 
80
- ```
81
- MODAL_TOKEN=your_modal_token_here
46
+ # The Modal token is automatically cleaned up after the container is created
82
47
  ```
83
48
 
84
- #### 2. Run the Proxy Service
49
+ ### Running the Modal Proxy Service
85
50
 
86
51
  ```bash
87
- # Start the proxy service
88
52
  python modal_proxy_service.py
89
53
  ```
90
54
 
91
- The service will start on port 5001 by default (to avoid conflicts with macOS AirPlay on port 5000).
92
-
93
- #### 3. Create an API Key for Clients
94
-
95
- When the service starts for the first time, it will generate an admin key. Use this key to create API keys for clients:
96
-
97
- ```bash
98
- # Create a new API key
99
- curl -X POST -H "X-Admin-Key: your_admin_key" http://localhost:5001/api/create-api-key
100
- ```
101
-
102
- #### 4. Using ngrok for Public Access (Optional)
103
-
104
- If you want to make your proxy service accessible from outside your network:
105
-
106
- ```bash
107
- # Install ngrok if you haven't already
108
- brew install ngrok # On macOS
109
-
110
- # Start ngrok to expose your proxy service
111
- ngrok http 5001
112
- ```
113
-
114
- Use the ngrok URL when configuring clients.
55
+ The service will automatically clean up Modal tokens on shutdown.
115
56
 
116
- ### Configuring the Client
57
+ ## Testing
117
58
 
118
- ```bash
119
- # Configure the proxy service
120
- ./gitarsenal.py proxy configure
121
- ```
122
-
123
- This will prompt you for the proxy service URL and API key.
124
-
125
- ### Checking Proxy Service Status
59
+ To test the Modal token cleanup functionality:
126
60
 
127
61
  ```bash
128
- # Check if the proxy service is running
129
- ./gitarsenal.py proxy status
62
+ python test_token_cleanup.py
130
63
  ```
131
64
 
132
- ### Creating a Sandbox through the Proxy
133
-
134
- ```bash
135
- # Create a sandbox through the proxy service
136
- ./gitarsenal.py proxy sandbox --gpu A10G --repo-url "https://github.com/username/repo.git" --wait
137
- ```
138
-
139
- ### Creating an SSH Container through the Proxy
140
-
141
- ```bash
142
- # Create an SSH container through the proxy service
143
- ./gitarsenal.py proxy ssh --gpu A10G --repo-url "https://github.com/username/repo.git" --wait
144
- ```
145
-
146
- ### Using the Proxy with Standard Commands
147
-
148
- You can also use the proxy service with the standard `sandbox` and `ssh` commands by adding the `--use-proxy` flag:
149
-
150
- ```bash
151
- # Create a sandbox using the proxy service
152
- ./gitarsenal.py sandbox --gpu A10G --repo-url "https://github.com/username/repo.git" --use-proxy
153
-
154
- # Create an SSH container using the proxy service
155
- ./gitarsenal.py ssh --gpu A10G --repo-url "https://github.com/username/repo.git" --use-proxy
156
- ```
157
-
158
- ## Managing Credentials
159
-
160
- You can manage your credentials using the following commands:
161
-
162
- ```bash
163
- # Set up all credentials
164
- ./gitarsenal.py credentials setup
165
-
166
- # Set a specific credential
167
- ./gitarsenal.py credentials set modal_token
168
-
169
- # View a credential (masked for security)
170
- ./gitarsenal.py credentials get modal_token
171
-
172
- # Clear a specific credential
173
- ./gitarsenal.py credentials clear huggingface_token
174
-
175
- # Clear all credentials
176
- ./gitarsenal.py credentials clear
177
-
178
- # List all saved credentials (without showing values)
179
- ./gitarsenal.py credentials list
180
- ```
181
-
182
- ## Security
183
-
184
- Your credentials are stored securely in `~/.gitarsenal/credentials.json` with restrictive file permissions. The file is only readable by your user account.
185
-
186
- Proxy configuration is stored in `~/.gitarsenal/proxy_config.json` with similar security measures.
187
-
188
- ## Troubleshooting
189
-
190
- ### Modal Authentication Issues
191
-
192
- GitArsenal CLI comes with a built-in Modal token for the freemium service, so you shouldn't encounter any authentication issues. If you do:
193
-
194
- 1. Ensure Modal is installed:
195
- ```bash
196
- pip install modal
197
- ```
198
-
199
- 2. Use the wrapper script to run commands with the built-in Modal token:
200
- ```bash
201
- python run_with_modal_token.py python modal_proxy_service.py
202
- ```
203
-
204
- The wrapper script automatically sets up the built-in Modal token, so you don't need to create your own Modal account or token.
205
-
206
- ### Proxy Service Issues
207
-
208
- If you're having issues with the proxy service:
209
-
210
- 1. Check if the proxy service is running:
211
- ```bash
212
- ./gitarsenal.py proxy status
213
- ```
214
-
215
- 2. Reconfigure the proxy service:
216
- ```bash
217
- ./gitarsenal.py proxy configure
218
- ```
219
-
220
- 3. Make sure you have a valid API key for the proxy service.
221
-
222
- 4. Check the proxy service logs:
223
- ```bash
224
- cat modal_proxy.log
225
- ```
226
-
227
- 5. If using ngrok, make sure the tunnel is active and use the correct URL.
228
-
229
- ### API Timeout Issues
230
-
231
- If the GitArsenal API times out when analyzing repositories, the tool will automatically use fallback setup commands based on the detected programming language and technologies.
232
-
233
- ### Other Issues
234
-
235
- 1. Check that your credentials are set up correctly:
236
- ```bash
237
- ./gitarsenal.py credentials list
238
- ```
239
-
240
- 2. If needed, clear and reset your credentials:
241
- ```bash
242
- ./gitarsenal.py credentials clear
243
- ./gitarsenal.py credentials setup
244
- ```
245
-
246
- ## License
247
-
248
- [MIT License](LICENSE)
65
+ This script verifies that the token cleanup process works correctly by:
66
+ 1. Setting up a test Modal token
67
+ 2. Verifying the token exists in environment and files
68
+ 3. Cleaning up the token
69
+ 4. Verifying the token has been removed
@@ -20,6 +20,8 @@ from dotenv import load_dotenv
20
20
  import uuid
21
21
  import sys
22
22
  from pathlib import Path
23
+ import subprocess
24
+ import signal
23
25
 
24
26
  # Add the current directory to the path so we can import the test_modalSandboxScript module
25
27
  current_dir = Path(__file__).parent.absolute()
@@ -208,6 +210,76 @@ def setup_modal_auth():
208
210
  logger.error(f"Error setting up Modal authentication: {e}")
209
211
  return False
210
212
 
213
+ def cleanup_modal_token():
214
+ """Delete Modal token files and environment variables after SSH container is started"""
215
+ logger.info("🧹 Cleaning up Modal token for security...")
216
+
217
+ try:
218
+ # Remove token from environment variables
219
+ if "MODAL_TOKEN_ID" in os.environ:
220
+ del os.environ["MODAL_TOKEN_ID"]
221
+ logger.info("✅ Removed MODAL_TOKEN_ID from environment")
222
+
223
+ if "MODAL_TOKEN" in os.environ:
224
+ del os.environ["MODAL_TOKEN"]
225
+ logger.info("✅ Removed MODAL_TOKEN from environment")
226
+
227
+ if "MODAL_TOKEN_SECRET" in os.environ:
228
+ del os.environ["MODAL_TOKEN_SECRET"]
229
+ logger.info("✅ Removed MODAL_TOKEN_SECRET from environment")
230
+
231
+ # Delete token files
232
+ from pathlib import Path
233
+ modal_dir = Path.home() / ".modal"
234
+ if modal_dir.exists():
235
+ # Delete token.json
236
+ token_file = modal_dir / "token.json"
237
+ if token_file.exists():
238
+ token_file.unlink()
239
+ logger.info(f"✅ Deleted token file at {token_file}")
240
+
241
+ # Delete token_alt.json if it exists
242
+ token_alt_file = modal_dir / "token_alt.json"
243
+ if token_alt_file.exists():
244
+ token_alt_file.unlink()
245
+ logger.info(f"✅ Deleted alternative token file at {token_alt_file}")
246
+
247
+ # Delete .modalconfig file
248
+ modalconfig_file = Path.home() / ".modalconfig"
249
+ if modalconfig_file.exists():
250
+ modalconfig_file.unlink()
251
+ logger.info(f"✅ Deleted .modalconfig file at {modalconfig_file}")
252
+
253
+ # Try to invalidate Modal sessions
254
+ try:
255
+ logger.info("🔑 Invalidating Modal sessions...")
256
+
257
+ # Try to directly modify Modal's session files
258
+ try:
259
+ # Check for session files in .modal directory
260
+ session_dir = modal_dir / "sessions"
261
+ if session_dir.exists():
262
+ import shutil
263
+ shutil.rmtree(session_dir)
264
+ logger.info(f"✅ Removed Modal sessions directory at {session_dir}")
265
+
266
+ # Also check for any other potential session files
267
+ for file in os.listdir(modal_dir) if modal_dir.exists() else []:
268
+ if "session" in file.lower() or "auth" in file.lower():
269
+ file_path = modal_dir / file
270
+ if file_path.is_file():
271
+ file_path.unlink()
272
+ logger.info(f"✅ Removed Modal session file: {file_path}")
273
+ except Exception as e:
274
+ logger.warning(f"⚠️ Error removing Modal sessions: {e}")
275
+
276
+ except Exception as e:
277
+ logger.warning(f"⚠️ Error during Modal session invalidation: {e}")
278
+
279
+ logger.info("✅ Modal token cleanup completed successfully")
280
+ except Exception as e:
281
+ logger.error(f"❌ Error during Modal token cleanup: {e}")
282
+
211
283
  @app.route('/api/health', methods=['GET'])
212
284
  def health_check():
213
285
  """Health check endpoint"""
@@ -502,6 +574,9 @@ def create_ssh_container():
502
574
  ssh_password=ssh_password
503
575
  )
504
576
 
577
+ # Clean up Modal token after container is created
578
+ cleanup_modal_token()
579
+
505
580
  if result:
506
581
  active_containers[container_id] = {
507
582
  "container_id": result.get("app_name"),
@@ -593,6 +668,17 @@ def terminate_container():
593
668
  logger.error(f"Error terminating container: {e}")
594
669
  return jsonify({"error": str(e)}), 500
595
670
 
671
+ # Register signal handlers for cleanup on shutdown
672
+ def signal_handler(sig, frame):
673
+ """Handle signals for graceful shutdown"""
674
+ logger.info(f"Received signal {sig}, cleaning up and shutting down...")
675
+ cleanup_modal_token()
676
+ sys.exit(0)
677
+
678
+ # Register signal handlers for common termination signals
679
+ signal.signal(signal.SIGINT, signal_handler) # Ctrl+C
680
+ signal.signal(signal.SIGTERM, signal_handler) # Termination request
681
+
596
682
  if __name__ == '__main__':
597
683
  # Check if Modal token is set
598
684
  if not MODAL_TOKEN:
@@ -981,13 +981,63 @@ def create_modal_sandbox(gpu_type, repo_url=None, repo_name=None, setup_commands
981
981
 
982
982
  # Check if this is a repo name that matches the end of current_dir
983
983
  # This prevents errors like "cd repo-name" when already in "/root/repo-name"
984
+ # BUT we need to be careful about nested directories like /root/litex/litex
984
985
  if (target_dir != "/" and target_dir != "." and target_dir != ".." and
985
986
  not target_dir.startswith("/") and not target_dir.startswith("./") and
986
987
  not target_dir.startswith("../") and current_dir.endswith("/" + target_dir)):
987
- print(f"⚠️ Detected redundant directory navigation: {cmd}")
988
- print(f"📂 Already in the correct directory: {current_dir}")
989
- print(f" Skipping unnecessary navigation command")
990
- return True, f"Already in directory {current_dir}", ""
988
+
989
+ # Advanced check: analyze directory contents to determine if navigation makes sense
990
+ print(f"🔍 Analyzing directory contents to determine navigation necessity...")
991
+
992
+ # Get current directory contents
993
+ current_contents_cmd = "ls -la"
994
+ current_result = sandbox.exec("bash", "-c", current_contents_cmd)
995
+ current_result.wait()
996
+ current_contents = _to_str(current_result.stdout) if current_result.stdout else ""
997
+
998
+ # Check if target directory exists
999
+ test_cmd = f"test -d \"{target_dir}\""
1000
+ test_result = sandbox.exec("bash", "-c", test_cmd)
1001
+ test_result.wait()
1002
+
1003
+ if test_result.returncode == 0:
1004
+ # Target directory exists, get its contents
1005
+ target_contents_cmd = f"ls -la \"{target_dir}\""
1006
+ target_result = sandbox.exec("bash", "-c", target_contents_cmd)
1007
+ target_result.wait()
1008
+ target_contents = _to_str(target_result.stdout) if target_result.stdout else ""
1009
+
1010
+ try:
1011
+ # Call LLM for analysis with the dedicated function
1012
+ llm_response = analyze_directory_navigation_with_llm(current_dir, target_dir, current_contents, target_contents, api_key)
1013
+
1014
+ # Extract decision from LLM response
1015
+ if llm_response and "NAVIGATE" in llm_response.upper():
1016
+ print(f"🤖 LLM Analysis: Navigation makes sense - contents are different")
1017
+ print(f"📂 Current: {current_dir}")
1018
+ print(f"🎯 Target: {target_dir}")
1019
+ print(f"🔄 Proceeding with navigation...")
1020
+ else:
1021
+ print(f"🤖 LLM Analysis: Navigation is redundant - contents are similar")
1022
+ print(f"⚠️ Detected redundant directory navigation: {cmd}")
1023
+ print(f"📂 Already in the correct directory: {current_dir}")
1024
+ print(f"✅ Skipping unnecessary navigation command")
1025
+ return True, f"Already in directory {current_dir}", ""
1026
+
1027
+ except Exception as e:
1028
+ print(f"⚠️ LLM analysis failed: {e}")
1029
+ print(f"🔄 Falling back to simple directory existence check...")
1030
+ # Fallback to simple check
1031
+ print(f"🔍 Detected nested directory '{target_dir}' exists in current location")
1032
+ print(f"📂 Current: {current_dir}")
1033
+ print(f"🎯 Target: {target_dir}")
1034
+ print(f"🔄 Proceeding with navigation to nested directory...")
1035
+ else:
1036
+ # No nested directory exists, so this is truly redundant
1037
+ print(f"⚠️ Detected redundant directory navigation: {cmd}")
1038
+ print(f"📂 Already in the correct directory: {current_dir}")
1039
+ print(f"✅ Skipping unnecessary navigation command")
1040
+ return True, f"Already in directory {current_dir}", ""
991
1041
 
992
1042
  # Remove any parenthetical text that could cause syntax errors in bash
993
1043
  if '(' in cmd:
@@ -2488,6 +2538,9 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2488
2538
  with app.run():
2489
2539
  ssh_container_function.remote()
2490
2540
 
2541
+ # Clean up Modal token after container is successfully created
2542
+ cleanup_modal_token()
2543
+
2491
2544
  return {
2492
2545
  "app_name": app_name,
2493
2546
  "ssh_password": ssh_password,
@@ -3172,6 +3225,144 @@ def find_entry_point(repo_dir):
3172
3225
 
3173
3226
  return None
3174
3227
 
3228
+ def analyze_directory_navigation_with_llm(current_dir, target_dir, current_contents, target_contents, api_key=None):
3229
+ """Use LLM to analyze if directory navigation makes sense"""
3230
+ if not api_key:
3231
+ # Try to get API key from environment
3232
+ api_key = os.environ.get("OPENAI_API_KEY")
3233
+
3234
+ if not api_key:
3235
+ print("⚠️ No OpenAI API key available for directory analysis")
3236
+ return None
3237
+
3238
+ # Create analysis prompt
3239
+ analysis_prompt = f"""
3240
+ I'm trying to determine if a 'cd {target_dir}' command makes sense.
3241
+
3242
+ CURRENT DIRECTORY: {current_dir}
3243
+ Current directory contents:
3244
+ {current_contents}
3245
+
3246
+ TARGET DIRECTORY: {target_dir}
3247
+ Target directory contents:
3248
+ {target_contents}
3249
+
3250
+ Please analyze if navigating to the target directory makes sense by considering:
3251
+ 1. Are the contents significantly different?
3252
+ 2. Does the target directory contain important files (like source code, config files, etc.)?
3253
+ 3. Is this likely a nested project directory or just a duplicate?
3254
+ 4. Would navigating provide access to different functionality or files?
3255
+
3256
+ Respond with only 'NAVIGATE' if navigation makes sense, or 'SKIP' if it's redundant.
3257
+ """
3258
+
3259
+ # Prepare the API request
3260
+ headers = {
3261
+ "Content-Type": "application/json",
3262
+ "Authorization": f"Bearer {api_key}"
3263
+ }
3264
+
3265
+ payload = {
3266
+ "model": "gpt-4",
3267
+ "messages": [
3268
+ {"role": "system", "content": "You are a directory navigation assistant. Analyze if navigating to a target directory makes sense based on the contents of both directories. Respond with only 'NAVIGATE' or 'SKIP'."},
3269
+ {"role": "user", "content": analysis_prompt}
3270
+ ],
3271
+ "temperature": 0.1,
3272
+ "max_tokens": 50
3273
+ }
3274
+
3275
+ try:
3276
+ print("🤖 Calling OpenAI for directory navigation analysis...")
3277
+ response = requests.post(
3278
+ "https://api.openai.com/v1/chat/completions",
3279
+ headers=headers,
3280
+ json=payload,
3281
+ timeout=30
3282
+ )
3283
+
3284
+ if response.status_code == 200:
3285
+ result = response.json()
3286
+ llm_response = result["choices"][0]["message"]["content"].strip()
3287
+ print(f"🤖 LLM Response: {llm_response}")
3288
+ return llm_response
3289
+ else:
3290
+ print(f"❌ OpenAI API error: {response.status_code} - {response.text}")
3291
+ return None
3292
+ except Exception as e:
3293
+ print(f"❌ Error calling OpenAI API: {e}")
3294
+ return None
3295
+
3296
+ def cleanup_modal_token():
3297
+ """Delete Modal token files and environment variables after SSH container is started"""
3298
+ print("🧹 Cleaning up Modal token for security...")
3299
+
3300
+ try:
3301
+ # Remove token from environment variables
3302
+ if "MODAL_TOKEN_ID" in os.environ:
3303
+ del os.environ["MODAL_TOKEN_ID"]
3304
+ print("✅ Removed MODAL_TOKEN_ID from environment")
3305
+
3306
+ if "MODAL_TOKEN" in os.environ:
3307
+ del os.environ["MODAL_TOKEN"]
3308
+ print("✅ Removed MODAL_TOKEN from environment")
3309
+
3310
+ if "MODAL_TOKEN_SECRET" in os.environ:
3311
+ del os.environ["MODAL_TOKEN_SECRET"]
3312
+ print("✅ Removed MODAL_TOKEN_SECRET from environment")
3313
+
3314
+ # Delete token files
3315
+ home_dir = os.path.expanduser("~")
3316
+ modal_dir = os.path.join(home_dir, ".modal")
3317
+ if os.path.exists(modal_dir):
3318
+ # Delete token.json
3319
+ token_file = os.path.join(modal_dir, "token.json")
3320
+ if os.path.exists(token_file):
3321
+ os.remove(token_file)
3322
+ print(f"✅ Deleted token file at {token_file}")
3323
+
3324
+ # Delete token_alt.json if it exists
3325
+ token_alt_file = os.path.join(modal_dir, "token_alt.json")
3326
+ if os.path.exists(token_alt_file):
3327
+ os.remove(token_alt_file)
3328
+ print(f"✅ Deleted alternative token file at {token_alt_file}")
3329
+
3330
+ # Delete .modalconfig file
3331
+ modalconfig_file = os.path.join(home_dir, ".modalconfig")
3332
+ if os.path.exists(modalconfig_file):
3333
+ os.remove(modalconfig_file)
3334
+ print(f"✅ Deleted .modalconfig file at {modalconfig_file}")
3335
+
3336
+ # Try to invalidate Modal sessions
3337
+ try:
3338
+ print("🔑 Invalidating Modal sessions...")
3339
+
3340
+ # As a last resort, try to directly modify Modal's session files
3341
+ try:
3342
+ # Check for session files in .modal directory
3343
+ session_dir = os.path.join(modal_dir, "sessions")
3344
+ if os.path.exists(session_dir):
3345
+ import shutil
3346
+ shutil.rmtree(session_dir)
3347
+ print(f"✅ Removed Modal sessions directory at {session_dir}")
3348
+
3349
+ # Also check for any other potential session files
3350
+ for file in os.listdir(modal_dir) if os.path.exists(modal_dir) else []:
3351
+ if "session" in file.lower() or "auth" in file.lower():
3352
+ file_path = os.path.join(modal_dir, file)
3353
+ if os.path.isfile(file_path):
3354
+ os.remove(file_path)
3355
+ print(f"✅ Removed Modal session file: {file_path}")
3356
+ except Exception as e:
3357
+ print(f"⚠️ Error removing Modal sessions: {e}")
3358
+
3359
+ except Exception as e:
3360
+ print(f"⚠️ Error during Modal session invalidation: {e}")
3361
+
3362
+ print("✅ Modal token cleanup completed successfully")
3363
+ except Exception as e:
3364
+ print(f"❌ Error during Modal token cleanup: {e}")
3365
+
3175
3366
  if __name__ == "__main__":
3176
3367
  # Parse command line arguments when script is run directly
3177
3368
  import argparse
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test Modal Token Cleanup
4
+
5
+ This script tests the Modal token cleanup functionality by:
6
+ 1. Setting up a Modal token
7
+ 2. Verifying the token exists in environment and files
8
+ 3. Cleaning up the token
9
+ 4. Verifying the token has been removed
10
+ """
11
+
12
+ import os
13
+ import sys
14
+ import json
15
+ from pathlib import Path
16
+ import time
17
+
18
+ # Add the parent directory to the path so we can import our modules
19
+ parent_dir = Path(__file__).parent.absolute()
20
+ sys.path.append(str(parent_dir))
21
+
22
+ # Import our cleanup function
23
+ from test_modalSandboxScript import cleanup_modal_token
24
+
25
+ def setup_test_token():
26
+ """Set up a test Modal token in environment and files"""
27
+ print("🔧 Setting up test Modal token...")
28
+
29
+ # Set test token in environment variables
30
+ test_token = "ak-testtoken12345"
31
+ os.environ["MODAL_TOKEN_ID"] = test_token
32
+ os.environ["MODAL_TOKEN"] = test_token
33
+ os.environ["MODAL_TOKEN_SECRET"] = "as-testsecret12345"
34
+
35
+ # Create token files
36
+ modal_dir = Path.home() / ".modal"
37
+ modal_dir.mkdir(exist_ok=True)
38
+
39
+ # Create token.json
40
+ token_file = modal_dir / "token.json"
41
+ with open(token_file, 'w') as f:
42
+ token_data = {
43
+ "token_id": test_token,
44
+ "token_secret": "as-testsecret12345"
45
+ }
46
+ json.dump(token_data, f)
47
+
48
+ # Create token_alt.json
49
+ token_alt_file = modal_dir / "token_alt.json"
50
+ with open(token_alt_file, 'w') as f:
51
+ token_data_alt = {
52
+ "id": test_token,
53
+ "secret": "as-testsecret12345"
54
+ }
55
+ json.dump(token_data_alt, f)
56
+
57
+ # Create .modalconfig file
58
+ modalconfig_file = Path.home() / ".modalconfig"
59
+ with open(modalconfig_file, 'w') as f:
60
+ f.write(f"token_id = {test_token}\n")
61
+ f.write(f"token_secret = as-testsecret12345\n")
62
+
63
+ print("✅ Test Modal token set up successfully")
64
+ return test_token
65
+
66
+ def verify_token_exists(test_token):
67
+ """Verify that the test token exists in environment and files"""
68
+ print("🔍 Verifying test token exists...")
69
+
70
+ # Check environment variables
71
+ env_token_id = os.environ.get("MODAL_TOKEN_ID")
72
+ env_token = os.environ.get("MODAL_TOKEN")
73
+ env_token_secret = os.environ.get("MODAL_TOKEN_SECRET")
74
+
75
+ if env_token_id != test_token:
76
+ print(f"❌ MODAL_TOKEN_ID mismatch: expected '{test_token}', got '{env_token_id}'")
77
+ return False
78
+
79
+ if env_token != test_token:
80
+ print(f"❌ MODAL_TOKEN mismatch: expected '{test_token}', got '{env_token}'")
81
+ return False
82
+
83
+ if not env_token_secret:
84
+ print("❌ MODAL_TOKEN_SECRET not set")
85
+ return False
86
+
87
+ # Check token files
88
+ modal_dir = Path.home() / ".modal"
89
+ token_file = modal_dir / "token.json"
90
+ if not token_file.exists():
91
+ print(f"❌ Token file not found at {token_file}")
92
+ return False
93
+
94
+ token_alt_file = modal_dir / "token_alt.json"
95
+ if not token_alt_file.exists():
96
+ print(f"❌ Alternative token file not found at {token_alt_file}")
97
+ return False
98
+
99
+ modalconfig_file = Path.home() / ".modalconfig"
100
+ if not modalconfig_file.exists():
101
+ print(f"❌ .modalconfig file not found at {modalconfig_file}")
102
+ return False
103
+
104
+ print("✅ Test token exists in environment and files")
105
+ return True
106
+
107
+ def verify_token_cleaned_up():
108
+ """Verify that the test token has been cleaned up"""
109
+ print("🔍 Verifying token cleanup...")
110
+
111
+ # Check environment variables
112
+ env_token_id = os.environ.get("MODAL_TOKEN_ID")
113
+ env_token = os.environ.get("MODAL_TOKEN")
114
+ env_token_secret = os.environ.get("MODAL_TOKEN_SECRET")
115
+
116
+ if env_token_id:
117
+ print(f"❌ MODAL_TOKEN_ID still exists: '{env_token_id}'")
118
+ return False
119
+
120
+ if env_token:
121
+ print(f"❌ MODAL_TOKEN still exists: '{env_token}'")
122
+ return False
123
+
124
+ if env_token_secret:
125
+ print(f"❌ MODAL_TOKEN_SECRET still exists: '{env_token_secret}'")
126
+ return False
127
+
128
+ # Check token files
129
+ modal_dir = Path.home() / ".modal"
130
+ token_file = modal_dir / "token.json"
131
+ if token_file.exists():
132
+ print(f"❌ Token file still exists at {token_file}")
133
+ return False
134
+
135
+ token_alt_file = modal_dir / "token_alt.json"
136
+ if token_alt_file.exists():
137
+ print(f"❌ Alternative token file still exists at {token_alt_file}")
138
+ return False
139
+
140
+ modalconfig_file = Path.home() / ".modalconfig"
141
+ if modalconfig_file.exists():
142
+ print(f"❌ .modalconfig file still exists at {modalconfig_file}")
143
+ return False
144
+
145
+ # Check if Modal sessions directory exists
146
+ session_dir = modal_dir / "sessions"
147
+ if session_dir.exists():
148
+ print(f"❌ Modal sessions directory still exists at {session_dir}")
149
+ return False
150
+
151
+ # Check for any other session or auth files
152
+ if modal_dir.exists():
153
+ for file in os.listdir(modal_dir):
154
+ if "session" in file.lower() or "auth" in file.lower():
155
+ print(f"❌ Modal session/auth file still exists: {modal_dir / file}")
156
+ return False
157
+
158
+ # Check if Modal CLI is still able to access tokens
159
+ try:
160
+ import subprocess
161
+ result = subprocess.run(
162
+ ["modal", "token", "current"],
163
+ capture_output=True,
164
+ text=True
165
+ )
166
+ if result.returncode == 0 and "No token found" not in result.stdout and "No token found" not in result.stderr:
167
+ print("❌ Modal CLI is still able to access tokens")
168
+ print(f"Output: {result.stdout}")
169
+ return False
170
+ except Exception as e:
171
+ print(f"⚠️ Error checking Modal CLI token status: {e}")
172
+
173
+ print("✅ Token has been cleaned up successfully")
174
+ return True
175
+
176
+ def run_test():
177
+ """Run the token cleanup test"""
178
+ print("🧪 Running Modal token cleanup test...")
179
+
180
+ # Set up test token
181
+ test_token = setup_test_token()
182
+
183
+ # Verify token exists
184
+ if not verify_token_exists(test_token):
185
+ print("❌ Test failed: Token setup verification failed")
186
+ return False
187
+
188
+ # Clean up token
189
+ print("🧹 Cleaning up token...")
190
+ cleanup_modal_token()
191
+
192
+ # Verify token has been cleaned up
193
+ if not verify_token_cleaned_up():
194
+ print("❌ Test failed: Token cleanup verification failed")
195
+ return False
196
+
197
+ print("✅ Test passed: Token cleanup works correctly")
198
+ return True
199
+
200
+ if __name__ == "__main__":
201
+ success = run_test()
202
+ sys.exit(0 if success else 1)
@@ -747,13 +747,29 @@ def create_modal_sandbox(gpu_type, repo_url=None, repo_name=None, setup_commands
747
747
 
748
748
  # Check if this is a repo name that matches the end of current_dir
749
749
  # This prevents errors like "cd repo-name" when already in "/root/repo-name"
750
+ # BUT we need to be careful about nested directories like /root/litex/litex
750
751
  if (target_dir != "/" and target_dir != "." and target_dir != ".." and
751
752
  not target_dir.startswith("/") and not target_dir.startswith("./") and
752
753
  not target_dir.startswith("../") and current_dir.endswith("/" + target_dir)):
753
- print(f"⚠️ Detected redundant directory navigation: {cmd}")
754
- print(f"📂 Already in the correct directory: {current_dir}")
755
- print(f"✅ Skipping unnecessary navigation command")
756
- return True, f"Already in directory {current_dir}", ""
754
+
755
+ # Additional check: verify if there's actually a nested directory with this name
756
+ # This prevents skipping legitimate navigation to nested directories
757
+ test_cmd = f"test -d \"{target_dir}\""
758
+ test_result = sandbox.exec("bash", "-c", test_cmd)
759
+ test_result.wait()
760
+
761
+ if test_result.returncode == 0:
762
+ # The nested directory exists, so this is NOT redundant
763
+ print(f"🔍 Detected nested directory '{target_dir}' exists in current location")
764
+ print(f"📂 Current: {current_dir}")
765
+ print(f"🎯 Target: {target_dir}")
766
+ print(f"🔄 Proceeding with navigation to nested directory...")
767
+ else:
768
+ # No nested directory exists, so this is truly redundant
769
+ print(f"⚠️ Detected redundant directory navigation: {cmd}")
770
+ print(f"📂 Already in the correct directory: {current_dir}")
771
+ print(f"✅ Skipping unnecessary navigation command")
772
+ return True, f"Already in directory {current_dir}", ""
757
773
 
758
774
  # Remove any parenthetical text that could cause syntax errors in bash
759
775
  if '(' in cmd: