gitarsenal-cli 1.2.2 โ 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 +1 -1
- package/python/README.md +39 -219
- package/python/__pycache__/test_modalSandboxScript.cpython-313.pyc +0 -0
- package/python/modal_proxy_service.py +60 -0
- package/python/test_modalSandboxScript.py +169 -4
- package/python/test_token_cleanup.py +174 -0
- package/test_modalSandboxScript.py +20 -4
package/package.json
CHANGED
package/python/README.md
CHANGED
@@ -1,248 +1,68 @@
|
|
1
|
-
# GitArsenal CLI
|
1
|
+
# GitArsenal CLI Python Modules
|
2
2
|
|
3
|
-
|
3
|
+
This directory contains Python modules for the GitArsenal CLI.
|
4
4
|
|
5
|
-
##
|
5
|
+
## Modal Integration
|
6
6
|
|
7
|
-
|
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
|
-
|
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
|
-
|
14
|
+
## Security Features
|
19
15
|
|
20
|
-
###
|
16
|
+
### Modal Token Cleanup
|
21
17
|
|
22
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
32
|
+
### Creating a Modal SSH Container
|
73
33
|
|
74
|
-
```
|
75
|
-
|
76
|
-
```
|
34
|
+
```python
|
35
|
+
from test_modalSandboxScript import create_modal_ssh_container
|
77
36
|
|
78
|
-
|
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
|
-
|
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
|
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
|
-
|
56
|
+
## Testing
|
117
57
|
|
118
|
-
|
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
|
-
|
129
|
-
./gitarsenal.py proxy status
|
61
|
+
python test_token_cleanup.py
|
130
62
|
```
|
131
63
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
Binary file
|
@@ -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"""
|
@@ -502,6 +548,9 @@ def create_ssh_container():
|
|
502
548
|
ssh_password=ssh_password
|
503
549
|
)
|
504
550
|
|
551
|
+
# Clean up Modal token after container is created
|
552
|
+
cleanup_modal_token()
|
553
|
+
|
505
554
|
if result:
|
506
555
|
active_containers[container_id] = {
|
507
556
|
"container_id": result.get("app_name"),
|
@@ -593,6 +642,17 @@ def terminate_container():
|
|
593
642
|
logger.error(f"Error terminating container: {e}")
|
594
643
|
return jsonify({"error": str(e)}), 500
|
595
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
|
+
|
596
656
|
if __name__ == '__main__':
|
597
657
|
# Check if Modal token is set
|
598
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
|
-
|
988
|
-
|
989
|
-
print(f"
|
990
|
-
|
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,118 @@ 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
|
+
print("โ
Modal token cleanup completed successfully")
|
3337
|
+
except Exception as e:
|
3338
|
+
print(f"โ Error during Modal token cleanup: {e}")
|
3339
|
+
|
3175
3340
|
if __name__ == "__main__":
|
3176
3341
|
# Parse command line arguments when script is run directly
|
3177
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
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
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:
|