gitarsenal-cli 1.2.1 โ†’ 1.2.3

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.1",
3
+ "version": "1.2.3",
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,68 @@
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. Runs automatically after SSH container creation
24
+ 4. Runs on service shutdown via signal handlers
27
25
 
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)
26
+ This prevents potential token leakage when containers are shared or when the service is stopped.
39
27
 
40
28
  ## Usage
41
29
 
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
30
+ To use the GitArsenal CLI Python modules, you can import them directly or use the provided scripts.
71
31
 
72
- Copy the example environment file and edit it:
32
+ ### Creating a Modal SSH Container
73
33
 
74
- ```bash
75
- cp .env.example .env
76
- ```
34
+ ```python
35
+ from test_modalSandboxScript import create_modal_ssh_container
77
36
 
78
- Edit the `.env` file to add your Modal token:
37
+ result = create_modal_ssh_container(
38
+ gpu_type="A10G",
39
+ repo_url="https://github.com/user/repo",
40
+ repo_name="repo",
41
+ setup_commands=["pip install -r requirements.txt"],
42
+ timeout_minutes=60
43
+ )
79
44
 
80
- ```
81
- MODAL_TOKEN=your_modal_token_here
45
+ # The Modal token is automatically cleaned up after the container is created
82
46
  ```
83
47
 
84
- #### 2. Run the Proxy Service
48
+ ### Running the Modal Proxy Service
85
49
 
86
50
  ```bash
87
- # Start the proxy service
88
51
  python modal_proxy_service.py
89
52
  ```
90
53
 
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.
54
+ The service will automatically clean up Modal tokens on shutdown.
115
55
 
116
- ### Configuring the Client
56
+ ## Testing
117
57
 
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
58
+ To test the Modal token cleanup functionality:
126
59
 
127
60
  ```bash
128
- # Check if the proxy service is running
129
- ./gitarsenal.py proxy status
61
+ python test_token_cleanup.py
130
62
  ```
131
63
 
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)
64
+ This script verifies that the token cleanup process works correctly by:
65
+ 1. Setting up a test Modal token
66
+ 2. Verifying the token exists in environment and files
67
+ 3. Cleaning up the token
68
+ 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,50 @@ 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
+ logger.info("โœ… Modal token cleanup completed successfully")
254
+ except Exception as e:
255
+ logger.error(f"โŒ Error during Modal token cleanup: {e}")
256
+
211
257
  @app.route('/api/health', methods=['GET'])
212
258
  def health_check():
213
259
  """Health check endpoint"""
@@ -223,6 +269,18 @@ def root():
223
269
  """Root endpoint for basic connectivity testing"""
224
270
  return jsonify({"status": "ok", "message": "Modal proxy service is running"})
225
271
 
272
+ @app.route('/api/modal-tokens', methods=['GET'])
273
+ def get_modal_tokens():
274
+ """Get Modal tokens (protected by API key)"""
275
+ if not authenticate_request():
276
+ return jsonify({"error": "Unauthorized"}), 401
277
+
278
+ # Return the server's Modal token
279
+ return jsonify({
280
+ "token_id": MODAL_TOKEN,
281
+ "token_secret": MODAL_TOKEN # For compatibility, use the same token
282
+ })
283
+
226
284
  @app.route('/api/create-api-key', methods=['POST'])
227
285
  def create_api_key():
228
286
  """Create a new API key (protected by admin key)"""
@@ -490,6 +548,9 @@ def create_ssh_container():
490
548
  ssh_password=ssh_password
491
549
  )
492
550
 
551
+ # Clean up Modal token after container is created
552
+ cleanup_modal_token()
553
+
493
554
  if result:
494
555
  active_containers[container_id] = {
495
556
  "container_id": result.get("app_name"),
@@ -581,6 +642,17 @@ def terminate_container():
581
642
  logger.error(f"Error terminating container: {e}")
582
643
  return jsonify({"error": str(e)}), 500
583
644
 
645
+ # Register signal handlers for cleanup on shutdown
646
+ def signal_handler(sig, frame):
647
+ """Handle signals for graceful shutdown"""
648
+ logger.info(f"Received signal {sig}, cleaning up and shutting down...")
649
+ cleanup_modal_token()
650
+ sys.exit(0)
651
+
652
+ # Register signal handlers for common termination signals
653
+ signal.signal(signal.SIGINT, signal_handler) # Ctrl+C
654
+ signal.signal(signal.SIGTERM, signal_handler) # Termination request
655
+
584
656
  if __name__ == '__main__':
585
657
  # Check if Modal token is set
586
658
  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:
@@ -2181,17 +2231,113 @@ def ssh_container_function(ssh_password, repo_url=None, repo_name=None, setup_co
2181
2231
  subprocess.run(["service", "ssh", "start"], check=True)
2182
2232
 
2183
2233
  # Now modify the create_modal_ssh_container function to use the standalone ssh_container_function
2234
+
2184
2235
  def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_commands=None,
2185
2236
  volume_name=None, timeout_minutes=60, ssh_password=None):
2186
- """Create a Modal SSH container with GPU support and proper tunneling"""
2237
+ """Create a Modal SSH container with GPU support and tunneling"""
2187
2238
 
2188
- # Check Modal authentication
2239
+ # Check if Modal is authenticated
2189
2240
  try:
2190
- modal.config.get_current_workspace_name()
2191
- print("โœ… Modal authentication verified")
2241
+ # Print all environment variables for debugging
2242
+ print("๐Ÿ” DEBUG: Checking environment variables")
2243
+ modal_token_id = os.environ.get("MODAL_TOKEN_ID")
2244
+ modal_token = os.environ.get("MODAL_TOKEN")
2245
+ print(f"๐Ÿ” MODAL_TOKEN_ID exists: {'Yes' if modal_token_id else 'No'}")
2246
+ print(f"๐Ÿ” MODAL_TOKEN exists: {'Yes' if modal_token else 'No'}")
2247
+ if modal_token_id:
2248
+ print(f"๐Ÿ” MODAL_TOKEN_ID length: {len(modal_token_id)}")
2249
+ if modal_token:
2250
+ print(f"๐Ÿ” MODAL_TOKEN length: {len(modal_token)}")
2251
+
2252
+ # Try to access Modal token to check authentication
2253
+ try:
2254
+ # Check if token is set in environment
2255
+ modal_token_id = os.environ.get("MODAL_TOKEN_ID")
2256
+ if not modal_token_id:
2257
+ print("โš ๏ธ MODAL_TOKEN_ID not found in environment.")
2258
+ # Try to get from MODAL_TOKEN
2259
+ modal_token = os.environ.get("MODAL_TOKEN")
2260
+ if modal_token:
2261
+ print("โœ… Found token in MODAL_TOKEN environment variable")
2262
+ os.environ["MODAL_TOKEN_ID"] = modal_token
2263
+ modal_token_id = modal_token
2264
+ print(f"โœ… Set MODAL_TOKEN_ID from MODAL_TOKEN (length: {len(modal_token)})")
2265
+
2266
+ if modal_token_id:
2267
+ print(f"โœ… Modal token found (length: {len(modal_token_id)})")
2268
+
2269
+ # Use the comprehensive fix_modal_token script
2270
+ try:
2271
+ # Execute the fix_modal_token.py script
2272
+ import subprocess
2273
+ print(f"๐Ÿ”„ Running fix_modal_token.py to set up Modal token...")
2274
+ result = subprocess.run(
2275
+ ["python", os.path.join(os.path.dirname(__file__), "fix_modal_token.py")],
2276
+ capture_output=True,
2277
+ text=True
2278
+ )
2279
+
2280
+ # Print the output
2281
+ print(result.stdout)
2282
+
2283
+ if result.returncode != 0:
2284
+ print(f"โš ๏ธ Warning: fix_modal_token.py exited with code {result.returncode}")
2285
+ if result.stderr:
2286
+ print(f"Error: {result.stderr}")
2287
+
2288
+ print(f"โœ… Modal token setup completed")
2289
+ except Exception as e:
2290
+ print(f"โš ๏ธ Error running fix_modal_token.py: {e}")
2291
+ else:
2292
+ print("โŒ No Modal token found in environment variables")
2293
+ # Try to get from file as a last resort
2294
+ try:
2295
+ home_dir = os.path.expanduser("~")
2296
+ modal_dir = os.path.join(home_dir, ".modal")
2297
+ token_file = os.path.join(modal_dir, "token.json")
2298
+ if os.path.exists(token_file):
2299
+ print(f"๐Ÿ” Found Modal token file at {token_file}")
2300
+ with open(token_file, 'r') as f:
2301
+ import json
2302
+ token_data = json.load(f)
2303
+ if "token_id" in token_data:
2304
+ modal_token_id = token_data["token_id"]
2305
+ os.environ["MODAL_TOKEN_ID"] = modal_token_id
2306
+ os.environ["MODAL_TOKEN"] = modal_token_id
2307
+ print(f"โœ… Loaded token from file (length: {len(modal_token_id)})")
2308
+ else:
2309
+ print("โŒ Token file does not contain token_id")
2310
+ else:
2311
+ print("โŒ Modal token file not found")
2312
+ except Exception as e:
2313
+ print(f"โŒ Error loading token from file: {e}")
2314
+
2315
+ if not os.environ.get("MODAL_TOKEN_ID"):
2316
+ print("โŒ Could not find Modal token in any location")
2317
+ return None
2318
+
2319
+ except Exception as e:
2320
+ print(f"โš ๏ธ Error checking Modal token: {e}")
2321
+ # Try to use the token from environment
2322
+ modal_token_id = os.environ.get("MODAL_TOKEN_ID")
2323
+ modal_token = os.environ.get("MODAL_TOKEN")
2324
+ if modal_token_id:
2325
+ print(f"๐Ÿ”„ Using MODAL_TOKEN_ID from environment (length: {len(modal_token_id)})")
2326
+ elif modal_token:
2327
+ print(f"๐Ÿ”„ Using MODAL_TOKEN from environment (length: {len(modal_token)})")
2328
+ os.environ["MODAL_TOKEN_ID"] = modal_token
2329
+ modal_token_id = modal_token
2330
+ else:
2331
+ print("โŒ No Modal token available. Cannot proceed.")
2332
+ return None
2333
+
2334
+ # Set it in both environment variables
2335
+ os.environ["MODAL_TOKEN_ID"] = modal_token_id
2336
+ os.environ["MODAL_TOKEN"] = modal_token_id
2337
+ print("โœ… Set both MODAL_TOKEN_ID and MODAL_TOKEN environment variables")
2192
2338
  except Exception as e:
2193
- print(f"โŒ Modal authentication failed: {e}")
2194
- return None
2339
+ print(f"โš ๏ธ Error checking Modal authentication: {e}")
2340
+ print("Continuing anyway, but Modal operations may fail")
2195
2341
 
2196
2342
  # Generate a unique app name with timestamp to avoid conflicts
2197
2343
  timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
@@ -2230,86 +2376,99 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2230
2376
  print(f"โš ๏ธ Could not setup volume '{volume_name}': {e}")
2231
2377
  print("โš ๏ธ Continuing without persistent volume")
2232
2378
  volume = None
2379
+ else:
2380
+ # Create a default volume for this session
2381
+ default_volume_name = f"ssh-vol-{timestamp}"
2382
+ print(f"๐Ÿ“ฆ Creating default volume: {default_volume_name}")
2383
+ try:
2384
+ volume = modal.Volume.from_name(default_volume_name, create_if_missing=True)
2385
+ volume_name = default_volume_name
2386
+ print(f"โœ… Default volume '{default_volume_name}' created")
2387
+ except Exception as e:
2388
+ print(f"โš ๏ธ Could not create default volume: {e}")
2389
+ print("โš ๏ธ Continuing without persistent volume")
2390
+ volume = None
2233
2391
 
2234
- # Create the Modal app
2235
- app = modal.App(app_name)
2392
+ # Print debug info for authentication
2393
+ print("๐Ÿ” Modal authentication debug info:")
2394
+ modal_token = os.environ.get("MODAL_TOKEN_ID")
2395
+ print(f" - MODAL_TOKEN_ID in env: {'Yes' if modal_token else 'No'}")
2396
+ print(f" - Token length: {len(modal_token) if modal_token else 'N/A'}")
2236
2397
 
2398
+ # Verify we can create a Modal app
2399
+ try:
2400
+ print("๐Ÿ” Testing Modal app creation...")
2401
+ app = modal.App(app_name)
2402
+ print("โœ… Created Modal app successfully")
2403
+ except Exception as e:
2404
+ print(f"โŒ Error creating Modal app: {e}")
2405
+ return None
2406
+
2237
2407
  # Create SSH-enabled image
2238
- ssh_image = (
2239
- modal.Image.debian_slim()
2240
- .apt_install(
2241
- "openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
2242
- "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
2243
- "gpg", "ca-certificates", "software-properties-common"
2244
- )
2245
- .pip_install("uv", "modal")
2246
- .run_commands(
2247
- # Create SSH directory
2248
- "mkdir -p /var/run/sshd",
2249
- "mkdir -p /root/.ssh",
2250
- "chmod 700 /root/.ssh",
2251
-
2252
- # Configure SSH server for password authentication
2253
- "sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
2254
- "sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
2255
- "sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config",
2256
- "sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
2257
-
2258
- # SSH keep-alive settings
2259
- "echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
2260
- "echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
2261
-
2262
- # Allow SSH on port 22
2263
- "echo 'Port 22' >> /etc/ssh/sshd_config",
2264
-
2265
- # Generate SSH host keys
2266
- "ssh-keygen -A",
2267
-
2268
- # Set up a nice bash prompt
2269
- "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
2408
+ try:
2409
+ print("๐Ÿ“ฆ Building SSH-enabled image...")
2410
+ ssh_image = (
2411
+ modal.Image.debian_slim()
2412
+ .apt_install(
2413
+ "openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
2414
+ "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
2415
+ "gpg", "ca-certificates", "software-properties-common"
2416
+ )
2417
+ .pip_install("uv", "modal") # Fast Python package installer and Modal
2418
+ .run_commands(
2419
+ # Create SSH directory
2420
+ "mkdir -p /var/run/sshd",
2421
+ "mkdir -p /root/.ssh",
2422
+ "chmod 700 /root/.ssh",
2423
+
2424
+ # Configure SSH server
2425
+ "sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
2426
+ "sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
2427
+ "sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
2428
+
2429
+ # SSH keep-alive settings
2430
+ "echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
2431
+ "echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
2432
+
2433
+ # Generate SSH host keys
2434
+ "ssh-keygen -A",
2435
+
2436
+ # Set up a nice bash prompt
2437
+ "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
2438
+ )
2270
2439
  )
2271
- )
2272
-
2440
+ print("โœ… SSH image built successfully")
2441
+ except Exception as e:
2442
+ print(f"โŒ Error building SSH image: {e}")
2443
+ return None
2444
+
2273
2445
  # Configure volumes if available
2274
2446
  volumes_config = {}
2275
2447
  if volume:
2276
2448
  volumes_config[volume_mount_path] = volume
2277
-
2278
- # Define the SSH container function with proper tunnel setup
2449
+
2450
+ # Define the SSH container function
2279
2451
  @app.function(
2280
2452
  image=ssh_image,
2281
- timeout=timeout_minutes * 60,
2453
+ timeout=timeout_minutes * 60, # Convert to seconds
2282
2454
  gpu=gpu_spec['gpu'],
2283
2455
  cpu=2,
2284
2456
  memory=8192,
2457
+ serialized=True,
2285
2458
  volumes=volumes_config if volumes_config else None,
2286
2459
  )
2287
- def ssh_container():
2288
- """Start SSH container with password authentication and tunnel."""
2460
+ def ssh_container_function():
2461
+ """Start SSH container with password authentication and optional setup."""
2289
2462
  import subprocess
2290
2463
  import time
2291
2464
  import os
2292
2465
 
2293
- print("๐Ÿ”ง Setting up SSH container...")
2294
-
2295
2466
  # Set root password
2296
- print(f"๐Ÿ” Setting root password...")
2297
2467
  subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
2298
2468
 
2299
2469
  # Start SSH service
2300
- print("๐Ÿš€ Starting SSH service...")
2301
2470
  subprocess.run(["service", "ssh", "start"], check=True)
2302
2471
 
2303
- # Verify SSH service is running
2304
- result = subprocess.run(["service", "ssh", "status"], capture_output=True, text=True)
2305
- if result.returncode == 0:
2306
- print("โœ… SSH service is running")
2307
- else:
2308
- print("โŒ SSH service failed to start")
2309
- print(result.stdout)
2310
- print(result.stderr)
2311
- return
2312
-
2313
2472
  # Clone repository if provided
2314
2473
  if repo_url:
2315
2474
  repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
@@ -2343,74 +2502,52 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2343
2502
  if e.stderr:
2344
2503
  print(f"โŒ Error: {e.stderr}")
2345
2504
 
2346
- # CRITICAL: Use unencrypted tunnel for SSH (port 22)
2347
- print("๐ŸŒ Creating unencrypted SSH tunnel...")
2348
-
2349
- # Use Modal's unencrypted tunnel for SSH protocol
2505
+ # Create SSH tunnel
2350
2506
  with modal.forward(22, unencrypted=True) as tunnel:
2507
+ host, port = tunnel.tcp_socket
2508
+
2351
2509
  print("\n" + "=" * 80)
2352
2510
  print("๐ŸŽ‰ SSH CONTAINER IS READY!")
2353
2511
  print("=" * 80)
2354
- print(f"๐ŸŒ SSH Host: {tunnel.tcp_socket[0]}")
2355
- print(f"๐Ÿ”Œ SSH Port: {tunnel.tcp_socket[1]}")
2512
+ print(f"๐ŸŒ SSH Host: {host}")
2513
+ print(f"๐Ÿ”Œ SSH Port: {port}")
2356
2514
  print(f"๐Ÿ‘ค Username: root")
2357
2515
  print(f"๐Ÿ” Password: {ssh_password}")
2358
2516
  print()
2359
2517
  print("๐Ÿ”— CONNECT USING THIS COMMAND:")
2360
- print(f"ssh -p {tunnel.tcp_socket[1]} root@{tunnel.tcp_socket[0]}")
2518
+ print(f"ssh -p {port} root@{host}")
2361
2519
  print("=" * 80)
2362
2520
 
2363
- # Store connection info in a file for later reference
2364
- connection_info = {
2365
- "host": tunnel.tcp_socket[0],
2366
- "port": tunnel.tcp_socket[1],
2367
- "username": "root",
2368
- "password": ssh_password,
2369
- "app_name": app_name,
2370
- "timestamp": datetime.datetime.now().isoformat()
2371
- }
2372
-
2373
- try:
2374
- with open(os.path.expanduser("~/.modal_ssh_connection"), "w") as f:
2375
- json.dump(connection_info, f, indent=2)
2376
- print(f"๐Ÿ“‹ Connection info saved to ~/.modal_ssh_connection")
2377
- except Exception as e:
2378
- print(f"โš ๏ธ Could not save connection info: {e}")
2379
-
2380
- # Keep the container and tunnel alive
2381
- print("โณ Container is running. Press Ctrl+C to stop...")
2382
- try:
2383
- while True:
2384
- time.sleep(30)
2385
- # Check if SSH service is still running
2386
- try:
2387
- subprocess.run(["service", "ssh", "status"], check=True,
2388
- capture_output=True)
2389
- except subprocess.CalledProcessError:
2390
- print("โš ๏ธ SSH service stopped, restarting...")
2391
- subprocess.run(["service", "ssh", "start"], check=True)
2392
- except KeyboardInterrupt:
2393
- print("\n๐Ÿ‘‹ Container stopping...")
2394
- return
2521
+ # Keep the container running
2522
+ while True:
2523
+ time.sleep(30)
2524
+ # Check if SSH service is still running
2525
+ try:
2526
+ subprocess.run(["service", "ssh", "status"], check=True,
2527
+ capture_output=True)
2528
+ except subprocess.CalledProcessError:
2529
+ print("โš ๏ธ SSH service stopped, restarting...")
2530
+ subprocess.run(["service", "ssh", "start"], check=True)
2395
2531
 
2396
2532
  # Run the container
2397
2533
  try:
2398
2534
  print("โณ Starting container... This may take 1-2 minutes...")
2399
2535
 
2400
- # Run the container function
2401
- with app.run():
2402
- ssh_container.remote()
2536
+ # Start the container in a new thread to avoid blocking
2537
+ with modal.enable_output():
2538
+ with app.run():
2539
+ ssh_container_function.remote()
2540
+
2541
+ # Clean up Modal token after container is successfully created
2542
+ cleanup_modal_token()
2403
2543
 
2404
2544
  return {
2405
2545
  "app_name": app_name,
2406
2546
  "ssh_password": ssh_password,
2407
2547
  "volume_name": volume_name
2408
2548
  }
2409
-
2410
2549
  except Exception as e:
2411
2550
  print(f"โŒ Error running container: {e}")
2412
- import traceback
2413
- traceback.print_exc()
2414
2551
  return None
2415
2552
 
2416
2553
  def fetch_setup_commands_from_api(repo_url):
@@ -3088,6 +3225,118 @@ def find_entry_point(repo_dir):
3088
3225
 
3089
3226
  return None
3090
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
+ print("โœ… Modal token cleanup completed successfully")
3337
+ except Exception as e:
3338
+ print(f"โŒ Error during Modal token cleanup: {e}")
3339
+
3091
3340
  if __name__ == "__main__":
3092
3341
  # Parse command line arguments when script is run directly
3093
3342
  import argparse
@@ -0,0 +1,174 @@
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
+ print("โœ… Token has been cleaned up successfully")
146
+ return True
147
+
148
+ def run_test():
149
+ """Run the token cleanup test"""
150
+ print("๐Ÿงช Running Modal token cleanup test...")
151
+
152
+ # Set up test token
153
+ test_token = setup_test_token()
154
+
155
+ # Verify token exists
156
+ if not verify_token_exists(test_token):
157
+ print("โŒ Test failed: Token setup verification failed")
158
+ return False
159
+
160
+ # Clean up token
161
+ print("๐Ÿงน Cleaning up token...")
162
+ cleanup_modal_token()
163
+
164
+ # Verify token has been cleaned up
165
+ if not verify_token_cleaned_up():
166
+ print("โŒ Test failed: Token cleanup verification failed")
167
+ return False
168
+
169
+ print("โœ… Test passed: Token cleanup works correctly")
170
+ return True
171
+
172
+ if __name__ == "__main__":
173
+ success = run_test()
174
+ 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: