mindroot 9.2.0__py3-none-any.whl → 9.3.0__py3-none-any.whl

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.

Potentially problematic release.


This version of mindroot might be problematic. Click here for more details.

@@ -21,7 +21,7 @@ def update_plugins(request: PluginUpdateRequest):
21
21
  with open('plugins.json', 'w') as file:
22
22
  json.dump(plugins_data, file, indent=2)
23
23
 
24
- plugins.load('plugin_manifest.json')
24
+ plugins.load('data/plugin_manifest.json')
25
25
 
26
26
  return {"message": "Plugins updated successfully"}
27
27
  except Exception as e:
@@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
12
12
  async def get_installed_plugin_metadata(plugin_name: str) -> dict:
13
13
  """Get metadata from main plugin manifest"""
14
14
  try:
15
- manifest_path = Path('plugin_manifest.json')
15
+ manifest_path = Path('data/plugin_manifest.json')
16
16
  logger.debug(f"Reading main plugin manifest from: {manifest_path}")
17
17
 
18
18
  if not manifest_path.exists():
@@ -142,7 +142,8 @@ async def middleware(request: Request, call_next):
142
142
  print("Error checking for static file", e)
143
143
  pass
144
144
  print("Did not find static file")
145
- if request.url.path in public_routes:
145
+ # Accept explicitly-registered public routes, _or_ the password-reset link which carries its own token
146
+ if request.url.path in public_routes or request.url.path.startswith('/reset-password'):
146
147
  print('Public route: ', request.url.path)
147
148
  return await call_next(request)
148
149
  elif any([request.url.path.startswith(path) for path in public_static]):
@@ -7,66 +7,6 @@ from lib.providers.services import service, ServiceProvider
7
7
 
8
8
  logger = logging.getLogger(__name__)
9
9
 
10
- # Use paths relative to the process working directory
11
- REQUEST_DIR = "data/password_resets/requests"
12
- GENERATED_DIR = "data/password_resets/generated"
13
- CHECK_INTERVAL_SECONDS = 30
14
-
15
- @service()
16
- async def process_password_reset_requests(context=None):
17
- """Processes password reset request files from a directory."""
18
- # Ensure directories exist
19
- os.makedirs(REQUEST_DIR, exist_ok=True)
20
- os.makedirs(GENERATED_DIR, exist_ok=True)
21
-
22
- sp = ServiceProvider()
23
- initiate_reset = sp.get('user_service.initiate_password_reset')
24
-
25
- if not (initiate_reset and callable(initiate_reset)):
26
- logger.error("Could not get 'user_service.initiate_password_reset' service.")
27
- return
28
-
29
- for filename in os.listdir(REQUEST_DIR):
30
- request_file_path = os.path.join(REQUEST_DIR, filename)
31
- if not os.path.isfile(request_file_path):
32
- continue
33
-
34
- try:
35
- with open(request_file_path, 'r') as f:
36
- data = json.load(f)
37
-
38
- username = data.get("username")
39
- is_admin_reset = data.get("is_admin_reset", False)
40
-
41
- if not username:
42
- raise ValueError("Username is missing from request file.")
43
-
44
- logger.info(f"Processing password reset request for user: {username}")
45
- token = await initiate_reset(username=username, is_admin_reset=is_admin_reset)
46
-
47
- reset_link = f"/user_service/reset-password/{token}"
48
- generated_file_path = os.path.join(GENERATED_DIR, f"{username}_{datetime.utcnow().strftime('%Y%m%d%H%M%S')}.json")
49
- with open(generated_file_path, 'w') as f:
50
- json.dump({"username": username, "reset_link": reset_link, "token": token}, f, indent=2)
51
-
52
- logger.info(f"Successfully generated password reset link for {username}.")
53
- os.remove(request_file_path)
54
-
55
- except Exception as e:
56
- logger.error(f"Failed to process reset request file {filename}: {e}")
57
- error_file_path = os.path.join(REQUEST_DIR, f"{filename}.error")
58
- os.rename(request_file_path, error_file_path)
59
-
60
- @service()
61
- async def start_file_watcher_service(context=None):
62
- """Starts a background task to watch for password reset request files."""
63
- logger.info("Starting password reset file watcher service.")
64
- async def watcher_loop():
65
- while True:
66
- try:
67
- await process_password_reset_requests()
68
- except Exception as e:
69
- logger.error(f"Error in password reset watcher loop: {e}")
70
- await asyncio.sleep(CHECK_INTERVAL_SECONDS)
71
-
72
- asyncio.create_task(watcher_loop())
10
+ # This file is kept for backward compatibility
11
+ # Password reset trigger functionality is now handled directly in router.py
12
+ logger.info("File trigger service loaded - functionality moved to router.py")
@@ -1,3 +1,4 @@
1
+ from typing import Optional
1
2
  from lib.providers.services import service
2
3
  from .models import UserAuth, PasswordResetToken
3
4
  import bcrypt
@@ -5,19 +6,47 @@ import json
5
6
  import os
6
7
  import secrets
7
8
  from datetime import datetime, timedelta
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
8
12
 
9
13
  USER_DATA_ROOT = "data/users"
10
14
  RESET_TOKEN_VALIDITY_HOURS = 1
11
15
 
12
16
  @service()
13
- async def initiate_password_reset(username: str, is_admin_reset: bool = False, context=None) -> str:
17
+ async def initiate_password_reset(username: str, is_admin_reset: bool = False, token: Optional[str] = None, context=None) -> str:
14
18
  """Initiates a password reset, generates a token, and stores it."""
19
+ # Log current working directory and absolute paths
20
+ cwd = os.getcwd()
21
+ abs_user_data_root = os.path.abspath(USER_DATA_ROOT)
15
22
  user_dir = os.path.join(USER_DATA_ROOT, username)
23
+ abs_user_dir = os.path.abspath(user_dir)
24
+
25
+ logger.info(f"=== PASSWORD RESET INITIATION ===")
26
+ logger.info(f"Current working directory: {cwd}")
27
+ logger.info(f"USER_DATA_ROOT (relative): {USER_DATA_ROOT}")
28
+ logger.info(f"USER_DATA_ROOT (absolute): {abs_user_data_root}")
29
+ logger.info(f"User directory (relative): {user_dir}")
30
+ logger.info(f"User directory (absolute): {abs_user_dir}")
31
+ logger.info(f"Initiating password reset for user: {username}")
32
+
16
33
  if not os.path.exists(user_dir):
34
+ logger.error(f"User directory not found: {user_dir} (absolute: {abs_user_dir})")
35
+ logger.error(f"Directory exists check: {os.path.exists(abs_user_dir)}")
17
36
  raise ValueError("User not found")
18
37
 
19
- token = secrets.token_urlsafe(32)
38
+ # List existing files in user directory
39
+ try:
40
+ existing_files = os.listdir(user_dir)
41
+ logger.info(f"Existing files in user directory: {existing_files}")
42
+ except Exception as e:
43
+ logger.error(f"Error listing user directory: {e}")
44
+
45
+ if token is None:
46
+ token = secrets.token_urlsafe(32)
47
+
20
48
  expires_at = datetime.utcnow() + timedelta(hours=RESET_TOKEN_VALIDITY_HOURS)
49
+ logger.info(f"Generated reset token for {username}, expires at: {expires_at.isoformat()}")
21
50
 
22
51
  reset_data = PasswordResetToken(
23
52
  token=token,
@@ -26,54 +55,178 @@ async def initiate_password_reset(username: str, is_admin_reset: bool = False, c
26
55
  )
27
56
 
28
57
  reset_file_path = os.path.join(user_dir, "password_reset.json")
58
+ abs_reset_file_path = os.path.abspath(reset_file_path)
59
+ logger.info(f"Saving reset token to: {reset_file_path} (absolute: {abs_reset_file_path})")
60
+
29
61
  with open(reset_file_path, 'w') as f:
30
62
  json.dump(reset_data.dict(), f, indent=2)
31
-
32
- # In a real application, you would email this token to the user.
33
- # For this implementation, we return it directly.
63
+
64
+ logger.info(f"Reset token file created successfully")
65
+
66
+ # Verify file was created
67
+ if os.path.exists(reset_file_path):
68
+ logger.info(f"Verified: Reset token file exists after creation")
69
+ try:
70
+ with open(reset_file_path, 'r') as f:
71
+ verify_data = json.load(f)
72
+ logger.info(f"Verified: Reset token file contents: {verify_data}")
73
+ except Exception as e:
74
+ logger.error(f"Error reading back reset token file: {e}")
75
+ else:
76
+ logger.error(f"ERROR: Reset token file was not created successfully!")
77
+
34
78
  return token
35
79
 
36
80
  @service()
37
81
  async def reset_password_with_token(token: str, new_password: str, context=None) -> bool:
38
82
  """Resets a user's password using a valid reset token."""
39
- for username in os.listdir(USER_DATA_ROOT):
83
+ # Log current working directory and absolute paths
84
+ cwd = os.getcwd()
85
+ abs_user_data_root = os.path.abspath(USER_DATA_ROOT)
86
+
87
+ logger.info(f"=== PASSWORD RESET ATTEMPT ===")
88
+ logger.info(f"Current working directory: {cwd}")
89
+ logger.info(f"USER_DATA_ROOT (relative): {USER_DATA_ROOT}")
90
+ logger.info(f"USER_DATA_ROOT (absolute): {abs_user_data_root}")
91
+ logger.info(f"Attempting password reset with token: {token[:10]}...{token[-10:]}")
92
+ logger.info(f"Token length: {len(token)}")
93
+
94
+ if not os.path.exists(USER_DATA_ROOT):
95
+ logger.error(f"USER_DATA_ROOT directory does not exist:")
96
+ logger.error(f" Relative path: {USER_DATA_ROOT}")
97
+ logger.error(f" Absolute path: {abs_user_data_root}")
98
+ logger.error(f" Exists check: {os.path.exists(abs_user_data_root)}")
99
+ raise ValueError("User data directory not found")
100
+
101
+ users_found = 0
102
+ tokens_checked = 0
103
+
104
+ try:
105
+ user_dirs = os.listdir(USER_DATA_ROOT)
106
+ logger.info(f"Found {len(user_dirs)} potential user directories: {user_dirs}")
107
+ except Exception as e:
108
+ logger.error(f"Error listing USER_DATA_ROOT: {e}")
109
+ raise ValueError("Error accessing user data")
110
+
111
+ for username in user_dirs:
40
112
  user_dir = os.path.join(USER_DATA_ROOT, username)
113
+ abs_user_dir = os.path.abspath(user_dir)
114
+ logger.info(f"Checking user directory: {user_dir} (absolute: {abs_user_dir})")
115
+
116
+ if not os.path.isdir(user_dir):
117
+ logger.debug(f"Skipping {username} - not a directory")
118
+ continue
119
+
120
+ users_found += 1
121
+
122
+ # List all files in this user directory
123
+ try:
124
+ user_files = os.listdir(user_dir)
125
+ logger.info(f"Files in user '{username}' directory: {user_files}")
126
+ except Exception as e:
127
+ logger.error(f"Error listing files in user directory {username}: {e}")
128
+ continue
129
+
41
130
  reset_file_path = os.path.join(user_dir, "password_reset.json")
131
+ abs_reset_file_path = os.path.abspath(reset_file_path)
132
+ logger.info(f"Looking for reset file: {reset_file_path} (absolute: {abs_reset_file_path})")
133
+ logger.info(f"Reset file exists: {os.path.exists(reset_file_path)}")
42
134
 
43
135
  if not os.path.exists(reset_file_path):
136
+ logger.info(f"No reset file found for user {username} - skipping")
137
+ continue
138
+
139
+ logger.info(f"Found reset file for user {username}, reading token data")
140
+
141
+ try:
142
+ with open(reset_file_path, 'r') as f:
143
+ reset_data_dict = json.load(f)
144
+ logger.info(f"Reset file contents for {username}: {reset_data_dict}")
145
+ reset_data = PasswordResetToken(**reset_data_dict)
146
+ except (json.JSONDecodeError, TypeError, ValueError) as e:
147
+ logger.error(f"Error parsing reset file for {username}: {e}")
148
+ continue
149
+ except Exception as e:
150
+ logger.error(f"Unexpected error reading reset file for {username}: {e}")
44
151
  continue
45
152
 
46
- with open(reset_file_path, 'r') as f:
153
+ tokens_checked += 1
154
+ stored_token = reset_data.token
155
+ logger.info(f"Comparing tokens for user {username}:")
156
+ logger.info(f" Provided token: '{token[:10]}...{token[-10:]}' (length: {len(token)})")
157
+ logger.info(f" Stored token: '{stored_token[:10]}...{stored_token[-10:]}' (length: {len(stored_token)})")
158
+ logger.info(f" Tokens match: {stored_token == token}")
159
+
160
+ # Also check for URL encoding issues
161
+ import urllib.parse
162
+ decoded_token = urllib.parse.unquote(token)
163
+ logger.info(f" URL decoded token: '{decoded_token[:10]}...{decoded_token[-10:]}' (length: {len(decoded_token)})")
164
+ logger.info(f" Decoded tokens match: {stored_token == decoded_token}")
165
+
166
+ if reset_data.token == token or reset_data.token == decoded_token:
167
+ logger.info(f"Token match found for user {username}!")
168
+ matched_token = token if reset_data.token == token else decoded_token
169
+ logger.info(f"Matched using {'original' if matched_token == token else 'URL-decoded'} token")
170
+
171
+ # Check expiration
47
172
  try:
48
- reset_data = PasswordResetToken(**json.load(f))
49
- except (json.JSONDecodeError, TypeError):
173
+ expires_at = datetime.fromisoformat(reset_data.expires_at)
174
+ current_time = datetime.utcnow()
175
+ logger.info(f"Token expires at: {expires_at}")
176
+ logger.info(f"Current time: {current_time}")
177
+ logger.info(f"Token expired: {expires_at < current_time}")
178
+
179
+ if expires_at < current_time:
180
+ logger.warning(f"Token expired for user {username}, removing reset file")
181
+ os.remove(reset_file_path)
182
+ raise ValueError("Password reset token has expired.")
183
+ except ValueError as e:
184
+ if "expired" in str(e):
185
+ raise
186
+ logger.error(f"Error parsing expiration date for {username}: {e}")
50
187
  continue
51
188
 
52
- if reset_data.token == token:
53
- if datetime.fromisoformat(reset_data.expires_at) < datetime.utcnow():
54
- os.remove(reset_file_path) # Expired token
55
- raise ValueError("Password reset token has expired.")
56
-
189
+ # Update password
57
190
  auth_file_path = os.path.join(user_dir, "auth.json")
191
+ abs_auth_file_path = os.path.abspath(auth_file_path)
192
+ logger.info(f"Updating password for user {username}")
193
+ logger.info(f"Auth file: {auth_file_path} (absolute: {abs_auth_file_path})")
194
+
58
195
  if not os.path.exists(auth_file_path):
196
+ logger.error(f"Auth file not found for user {username}: {auth_file_path}")
59
197
  os.remove(reset_file_path)
60
198
  raise FileNotFoundError("User auth file not found.")
61
199
 
62
- with open(auth_file_path, 'r+') as auth_file:
63
- auth_data_dict = json.load(auth_file)
64
- auth_data = UserAuth(**auth_data_dict)
65
-
66
- new_password_hash = bcrypt.hashpw(new_password.encode(), bcrypt.gensalt()).decode()
67
- auth_data.password_hash = new_password_hash
200
+ try:
201
+ with open(auth_file_path, 'r+') as auth_file:
202
+ auth_data_dict = json.load(auth_file)
203
+ auth_data = UserAuth(**auth_data_dict)
68
204
 
69
- if reset_data.is_admin_reset and 'admin' not in auth_data.roles:
70
- auth_data.roles.append('admin')
205
+ new_password_hash = bcrypt.hashpw(new_password.encode(), bcrypt.gensalt()).decode()
206
+ auth_data.password_hash = new_password_hash
207
+ logger.info(f"Password hash updated for user {username}")
71
208
 
72
- auth_file.seek(0)
73
- json.dump(auth_data.dict(), auth_file, indent=2, default=str)
74
- auth_file.truncate()
209
+ if reset_data.is_admin_reset and 'admin' not in auth_data.roles:
210
+ auth_data.roles.append('admin')
211
+ logger.info(f"Added admin role to user {username}")
75
212
 
76
- os.remove(reset_file_path) # Invalidate token after use
77
- return True
213
+ auth_file.seek(0)
214
+ json.dump(auth_data.dict(), auth_file, indent=2, default=str)
215
+ auth_file.truncate()
216
+ logger.info(f"Auth file updated for user {username}")
78
217
 
218
+ os.remove(reset_file_path)
219
+ logger.info(f"Reset token file removed for user {username}")
220
+ logger.info(f"Password reset completed successfully for user {username}")
221
+ return True
222
+
223
+ except Exception as e:
224
+ logger.error(f"Error updating auth file for user {username}: {e}")
225
+ raise ValueError(f"Error updating user authentication: {e}")
226
+
227
+ logger.warning(f"=== TOKEN NOT FOUND ===")
228
+ logger.warning(f"Token not found after checking {users_found} users and {tokens_checked} tokens")
229
+ logger.warning(f"Provided token: '{token}'")
230
+ logger.warning(f"Current working directory: {cwd}")
231
+ logger.warning(f"Searched in: {abs_user_data_root}")
79
232
  raise ValueError("Invalid password reset token.")
@@ -1,36 +1,96 @@
1
1
  from fastapi import APIRouter, Request, Form, Depends
2
2
  from fastapi.responses import HTMLResponse, RedirectResponse
3
3
  from lib.templates import render
4
+ import os
5
+ import json
6
+ import logging
7
+ # Import services directly to avoid coroutine-call confusion
4
8
  from .password_reset_service import reset_password_with_token, initiate_password_reset
5
9
  from lib.providers.services import service_manager
6
10
  from lib.providers import ProviderManager
11
+ from lib.route_decorators import public_route
7
12
 
13
+ logger = logging.getLogger(__name__)
8
14
  router = APIRouter()
9
15
 
10
- @router.get("/reset-password/{token}")
11
- async def get_reset_password_form(request: Request, token: str):
12
- return await render('reset_password', {"request": request, "token": token, "error": None, "success": False})
16
+ @public_route()
17
+ @router.get("/reset-password/{filename}")
18
+ async def get_reset_password_form_by_file(request: Request, filename: str):
19
+ """Show password reset form if trigger file exists"""
20
+ trigger_dir = "data/password_resets"
21
+ file_path = os.path.join(trigger_dir, f"{filename}")
22
+ print("file path", file_path)
13
23
 
14
- @router.post("/reset-password/{token}")
15
- async def handle_reset_password(request: Request, token: str, password: str = Form(...), confirm_password: str = Form(...), services: ProviderManager = Depends(lambda: service_manager)):
16
- if password != confirm_password:
17
- return await render('reset_password', {"request": request, "token": token, "error": "Passwords do not match.", "success": False})
24
+ # Validate filename format
25
+ if not filename or not filename.replace('-', '').replace('_', '').isalnum():
26
+ print('1')
27
+ html = await render('reset_password', {"request": request, "token": filename, "error": "Invalid file format", "success": False})
28
+ return HTMLResponse(content=html)
29
+
30
+ if not os.path.exists(file_path) or not os.path.isfile(file_path):
31
+ print('2')
32
+ html = await render('reset_password', {"request": request, "token": filename, "error": "Reset file not found or expired", "success": False})
33
+ return HTMLResponse(content=html)
34
+
35
+ # File exists and is actually a file, show the form
36
+ html = await render('reset_password', {"request": request, "token": filename, "error": None, "success": False})
37
+ return HTMLResponse(content=html)
18
38
 
39
+ @public_route()
40
+ @router.post("/reset-password/{filename}")
41
+ async def handle_reset_password_by_file(request: Request, filename: str, password: str = Form(...), confirm_password: str = Form(...), services: ProviderManager = Depends(lambda: service_manager)):
42
+ """Handle password reset using trigger file"""
43
+ trigger_dir = "data/password_resets"
44
+ file_path = os.path.join(trigger_dir, f"{filename}")
45
+
46
+ # Validate filename format
47
+ if not filename or not filename.replace('-', '').replace('_', '').isalnum():
48
+ html = await render('reset_password', {"request": request, "token": filename, "error": "Invalid file format", "success": False})
49
+ return HTMLResponse(content=html)
50
+
51
+ if password != confirm_password:
52
+ html = await render('reset_password', {"request": request, "token": filename, "error": "Passwords do not match.", "success": False})
53
+ return HTMLResponse(content=html)
54
+
55
+ if not os.path.exists(file_path) or not os.path.isfile(file_path):
56
+ html = await render('reset_password', {"request": request, "token": filename, "error": "Reset file not found or expired.", "success": False})
57
+ return HTMLResponse(content=html)
58
+
19
59
  try:
20
- success = await services.get('user_service.reset_password_with_token')(token=token, new_password=password)
60
+ # Read the trigger file
61
+ with open(file_path, 'r') as f:
62
+ data = f.read().strip()
63
+
64
+ # Parse the file content: "username is_admin_reset"
65
+ parts = data.split(' ')
66
+ if len(parts) != 2:
67
+ html = await render('reset_password', {"request": request, "token": filename, "error": "Invalid reset file format.", "success": False})
68
+ return HTMLResponse(content=html)
69
+
70
+ username, is_admin_reset_str = parts
71
+ is_admin_reset = is_admin_reset_str.lower() == 'true'
72
+
73
+ if not username:
74
+ html = await render('reset_password', {"request": request, "token": filename, "error": "Invalid reset file format.", "success": False})
75
+ return HTMLResponse(content=html)
76
+
77
+ logger.info(f"Processing password reset for user: {username} from file: {filename}")
78
+
79
+ # Generate token and reset password
80
+ token = await initiate_password_reset(username=username, is_admin_reset=is_admin_reset)
81
+ success = await reset_password_with_token(token=token, new_password=password)
82
+
21
83
  if success:
22
- return await render('reset_password', {"request": request, "token": token, "error": None, "success": True})
84
+ # Delete the trigger file
85
+ os.remove(file_path)
86
+ logger.info(f"Successfully reset password for {username}, removed trigger file: {filename}")
87
+ html = await render('reset_password', {"request": request, "token": filename, "error": None, "success": True})
88
+ return HTMLResponse(content=html)
23
89
  else:
24
- return await render('reset_password', {"request": request, "token": token, "error": "Invalid or expired token.", "success": False})
25
- except ValueError as e:
26
- return await render('reset_password', {"request": request, "token": token, "error": str(e), "success": False})
27
-
28
- # This is an admin-only function to generate a reset link.
29
- # In a real app, this would be more protected.
30
- @router.get("/admin/initiate-reset/{username}")
31
- async def admin_initiate_reset(username: str, services: ProviderManager = Depends(lambda: service_manager)):
32
- try:
33
- token = await services.get('user_service.initiate_password_reset')(username=username)
34
- return HTMLResponse(f'<h1>Password Reset Link</h1><p>Share this link with the user: <a href="/user_service/reset-password/{token}">/user_service/reset-password/{token}</a></p>')
35
- except ValueError as e:
36
- return HTMLResponse(f'<h1>Error</h1><p>{str(e)}</p>', status_code=404)
90
+ html = await render('reset_password', {"request": request, "token": filename, "error": "Password reset failed.", "success": False})
91
+ return HTMLResponse(content=html)
92
+
93
+ except Exception as e:
94
+ logger.error(f"Error processing trigger file {filename}: {e}")
95
+ html = await render('reset_password', {"request": request, "token": filename, "error": f"Error processing reset: {str(e)}", "success": False})
96
+ return HTMLResponse(content=html)
@@ -1,9 +1,10 @@
1
1
  import json
2
2
  import os
3
+ import shutil
3
4
  from datetime import datetime
4
5
 
5
6
  # Central definition of manifest file location
6
- MANIFEST_FILE = 'plugin_manifest.json'
7
+ MANIFEST_FILE = 'data/plugin_manifest.json'
7
8
 
8
9
  def load_plugin_manifest():
9
10
  """Load the plugin manifest file.
@@ -68,12 +69,20 @@ def update_plugin_manifest(plugin_name, source, source_path, remote_source=None,
68
69
 
69
70
  def create_default_plugin_manifest():
70
71
  """Create a new default manifest file."""
71
- # read from default_plugin_manifest.json in same dir as this file
72
- default_manifest_path = os.path.join(os.path.dirname(__file__), 'default_plugin_manifest.json')
73
- with open(default_manifest_path, 'r') as f:
74
- default_manifest = json.load(f)
75
- save_plugin_manifest(default_manifest)
76
-
72
+ # First check if there's an existing manifest in the root directory
73
+ old_manifest_path = 'plugin_manifest.json'
74
+ if os.path.exists(old_manifest_path):
75
+ # Move existing manifest from root to data directory
76
+ os.makedirs(os.path.dirname(MANIFEST_FILE), exist_ok=True)
77
+ shutil.move(old_manifest_path, MANIFEST_FILE)
78
+ else:
79
+ # No existing manifest, create from default template
80
+ # read from default_plugin_manifest.json in same dir as this file
81
+ default_manifest_path = os.path.join(os.path.dirname(__file__), 'default_plugin_manifest.json')
82
+ with open(default_manifest_path, 'r') as f:
83
+ default_manifest = json.load(f)
84
+ os.makedirs(os.path.dirname(MANIFEST_FILE), exist_ok=True)
85
+ save_plugin_manifest(default_manifest)
77
86
  def toggle_plugin_state(plugin_name, enabled):
78
87
  """Toggle a plugin's enabled state.
79
88
 
mindroot/migrate.py ADDED
@@ -0,0 +1,30 @@
1
+ import os
2
+ import shutil
3
+ from pathlib import Path
4
+
5
+ def migrate_plugin_manifest():
6
+ """Migrate plugin_manifest.json from root to data/ directory if needed."""
7
+ root_manifest = 'plugin_manifest.json'
8
+ data_manifest = 'data/plugin_manifest.json'
9
+
10
+ # If the new location already exists, no migration needed
11
+ if os.path.exists(data_manifest):
12
+ print(f"Plugin manifest already exists at {data_manifest}")
13
+ return
14
+
15
+ # If old location exists, move it to new location
16
+ if os.path.exists(root_manifest):
17
+ print(f"Migrating plugin manifest from {root_manifest} to {data_manifest}")
18
+ # Ensure data directory exists
19
+ os.makedirs('data', exist_ok=True)
20
+ # Move the file
21
+ shutil.move(root_manifest, data_manifest)
22
+ print(f"Plugin manifest migration complete")
23
+ else:
24
+ print("No existing plugin manifest found to migrate")
25
+
26
+ def run_migrations():
27
+ """Run all necessary migrations."""
28
+ print("Running MindRoot migrations...")
29
+ migrate_plugin_manifest()
30
+ print("Migrations complete")
mindroot/server.py CHANGED
@@ -14,6 +14,7 @@ import socket
14
14
  from fastapi.middleware.cors import CORSMiddleware
15
15
  from starlette.middleware.base import BaseHTTPMiddleware
16
16
  from dotenv import load_dotenv
17
+ from .migrate import run_migrations
17
18
 
18
19
  # Load environment variables from .env file at the start
19
20
  # Set override=True to make .env variables override existing environment variables
@@ -35,6 +36,7 @@ def get_project_root():
35
36
  def create_directories():
36
37
  root = get_project_root()
37
38
  directories = [
39
+ "data",
38
40
  "imgs",
39
41
  "models",
40
42
  "models/face",
@@ -102,6 +104,9 @@ class HeaderMiddleware(BaseHTTPMiddleware):
102
104
 
103
105
  def main():
104
106
  global app
107
+
108
+ # Run migrations first, before anything else
109
+ run_migrations()
105
110
 
106
111
  cmd_args = parse_args()
107
112
  # save ALL parsed args in app state
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mindroot
3
- Version: 9.2.0
3
+ Version: 9.3.0
4
4
  Summary: MindRoot AI Agent Framework
5
5
  Requires-Python: >=3.9
6
6
  License-File: LICENSE
@@ -1,5 +1,6 @@
1
1
  mindroot/__init__.py,sha256=OrFRGt_fdSYjolLXUzjSX2CIn1cOAm6l47ENNAkwmgQ,83
2
- mindroot/server.py,sha256=vFJSZ4FPI7YBWDlBfGihNMh1IiwBo9R0Sz8dM15cm4k,4888
2
+ mindroot/migrate.py,sha256=kVmcvtXkXRj0weVvPfwTKvdhv1VVYgPLuFnOY0XXxn4,1071
3
+ mindroot/server.py,sha256=mKqO1BNT9i5N5xTuLXPLXK7YAF-9AXNMeDP4rODBGJI,5015
3
4
  mindroot/coreplugins/admin/__init__.py,sha256=388n_hMskU0TnZ4xT10US_kFkya-EPBjWcv7AZf_HOk,74
4
5
  mindroot/coreplugins/admin/agent_importer.py,sha256=8hQLO64iKtPA5gv9-mLUfUCcnRp3IH9-sTfgkPsrmqo,6376
5
6
  mindroot/coreplugins/admin/agent_router.py,sha256=mstYUURNRb9MpfNwqMYC7WY3FOQJ-5H0TNNz4mDSjFM,9220
@@ -8,7 +9,7 @@ mindroot/coreplugins/admin/persona_handler.py,sha256=eRYmmOmupLJuiujWN35SS2nwRgo
8
9
  mindroot/coreplugins/admin/persona_router.py,sha256=tyPxBe-pRnntHo_xyazvnwKZxHmvEI5_Fsu0YkZbrXA,6036
9
10
  mindroot/coreplugins/admin/plugin_manager.py,sha256=ZXzxSNY6Y0_PXvnZSsSK3WDTkgDJNDuDlFP8y-F72h8,15355
10
11
  mindroot/coreplugins/admin/plugin_manager_backup.py,sha256=pUnh-EW4BMOrL2tYUq1X2BC65aoTBhDM-o2-mlDYXfU,24583
11
- mindroot/coreplugins/admin/plugin_router.py,sha256=EEDIM0MMPhqeej8Lu8cazYgzaXH1sKZLy_6kBOwrUgY,1020
12
+ mindroot/coreplugins/admin/plugin_router.py,sha256=SAxq2ladUyfENlzVr4rSZ0itMKvBJPl37mxgwjfQQD8,1025
12
13
  mindroot/coreplugins/admin/router.py,sha256=Wzeri92Rv39YKTYflMZ3E1pnYy959KHWRjS8D4Xgaao,1580
13
14
  mindroot/coreplugins/admin/server_router.py,sha256=aZ5v7QBcK1LJ2yiciUA4elY26iSGl-8m03L1h3ogvP4,4533
14
15
  mindroot/coreplugins/admin/service_models.py,sha256=ccYdLjWlEbbkIq9y9T5TL8NGSO6m1LLiJb9J2wkbenY,3354
@@ -1310,7 +1311,7 @@ mindroot/coreplugins/index/utils.py,sha256=UF30B_XM3FNHjwn_BnxaW9yEjwZl-TjyDBcUD
1310
1311
  mindroot/coreplugins/index/handlers/__init__.py,sha256=MPoCNJooyIGXwD9lXyIR8RQwUE5MrPj4wFpJVIzPCtM,421
1311
1312
  mindroot/coreplugins/index/handlers/agent_ops.py,sha256=6faXdvDwZfZSsev5BLlKxJkDo4BNskVNAstWt3_T6kA,3849
1312
1313
  mindroot/coreplugins/index/handlers/index_ops.py,sha256=VvGev9qKMtXK0LJnyO4s0fKeJwFLlXIBI3SPgYIGMD8,3898
1313
- mindroot/coreplugins/index/handlers/plugin_ops.py,sha256=KT5Ga8Q42rBfkaqyDd4VPtu3x1Ax7mEApS8a-xIhq2E,5599
1314
+ mindroot/coreplugins/index/handlers/plugin_ops.py,sha256=lYSNAvdbXtaTdJToe5Sn30H7PmkKrB3vP34g4NuUkS0,5604
1314
1315
  mindroot/coreplugins/index/handlers/publish.py,sha256=J4SiDSvaXpYV7My66nFjD1jHTVeDsV986iIwUAUlLok,4684
1315
1316
  mindroot/coreplugins/index/indices/default/index.json,sha256=2zR869dAVESvpGuRATZXaaQkh7n2oqkjc3DY2VcqXgc,37991
1316
1317
  mindroot/coreplugins/index/indices/default/personas/Assistant/avatar.png,sha256=AsT2_jjGpZvEhzTEwSVhEShSYaIBhcUDrlj_bAG_HVY,1169266
@@ -1694,7 +1695,7 @@ mindroot/coreplugins/index/static/js/lit-html/node/directives/until.js.map,sha25
1694
1695
  mindroot/coreplugins/index/static/js/lit-html/node/directives/when.js,sha256=NLe0NJ-6jqjVDUrT_DzmSpREsRaLo1yarzdYcV_5xHY,181
1695
1696
  mindroot/coreplugins/index/static/js/lit-html/node/directives/when.js.map,sha256=tOonih_-EaqrunhNGshA9xN--WIVdGikjg8MkVp0itQ,1534
1696
1697
  mindroot/coreplugins/jwt_auth/__init__.py,sha256=qFCBnx0oAKTtMSXiPEa7VXOIlWDTU-5CY0XvodgSUlM,79
1697
- mindroot/coreplugins/jwt_auth/middleware.py,sha256=Is8haTLe9K4PdN8hrpoN9lqee6fYfI8Viww6oZfTNIk,9227
1698
+ mindroot/coreplugins/jwt_auth/middleware.py,sha256=TPz1MZch6BJXtIMlmdQqUO3-xbUy9TMqFDTU8uetfzQ,9388
1698
1699
  mindroot/coreplugins/jwt_auth/mod.py,sha256=AfRDh9vyGGTE0qLdEOXl2TZYufYxqjsE34uIDQXq--o,1039
1699
1700
  mindroot/coreplugins/jwt_auth/role_checks.py,sha256=bruZIIBSOvXNWB1YZ2s5btrbbXNf18w6MdORpJByV60,1555
1700
1701
  mindroot/coreplugins/jwt_auth/router.py,sha256=ecXYao_UG33UjQF15Hi-tf_X0eFsqLEldyqGpt7JNSw,1162
@@ -1762,13 +1763,13 @@ mindroot/coreplugins/usage/templates/usage.jinja2,sha256=bSAjGbgx-hgzkPR0u1OIoJi
1762
1763
  mindroot/coreplugins/user_service/__init__.py,sha256=YdQYM2GS6I2vB6r4ilODQjcV6LKTnKlCmo1EbfCQeVE,171
1763
1764
  mindroot/coreplugins/user_service/admin_init.py,sha256=wQ28wmohiER0iMEyz4kypSbSDkD38LE7WXSgZyo0-pA,3923
1764
1765
  mindroot/coreplugins/user_service/email_service.py,sha256=ptcnvPIxF7kY9NgdDlNZYGirzi6ruY8sWmuMp-fHLgA,1591
1765
- mindroot/coreplugins/user_service/file_trigger_service.py,sha256=HyIvY_6UHE1oJgxDx-PsPcQa1l2cG9Cg4P_EvsnMVCI,2853
1766
+ mindroot/coreplugins/user_service/file_trigger_service.py,sha256=ryHY_YBlQYLGyOO59PKqC5ijy0DOcaXLtvIQOaumELE,381
1766
1767
  mindroot/coreplugins/user_service/hooks.py,sha256=4di2j0tVhEKplDaG9fTlRtErIflZPYBfC3iHGBtUgh0,875
1767
1768
  mindroot/coreplugins/user_service/mod.py,sha256=CW0CtNp4EOg3HsNS-PlwVVy4F5q0v0RJyAvifKVhFRM,4856
1768
1769
  mindroot/coreplugins/user_service/models.py,sha256=s_dy881Ob8Ng94PQ-Iw9lYq306l3QsnDLVk7e3OED5w,1137
1769
- mindroot/coreplugins/user_service/password_reset_service.py,sha256=K77GPP-APQOsqRxMzoaOb5OA7MCwubSMdRNcau7BJ3M,2992
1770
+ mindroot/coreplugins/user_service/password_reset_service.py,sha256=KsZ4VxCplJHWI0wNc-8dUiWNV6l74FIw3HiWCSdP8aI,10537
1770
1771
  mindroot/coreplugins/user_service/role_service.py,sha256=e6XrxhMC4903C-Y515XSC544uXAik6-CSee-TIPGIwA,2329
1771
- mindroot/coreplugins/user_service/router.py,sha256=IH89Ahk9T9WyAkvEi7Kgv6EIJ0jKOuN6u6sZ32mfDRs,2201
1772
+ mindroot/coreplugins/user_service/router.py,sha256=7TxaEYswj4NmQanfW8T15HTu3FoYMR1m3OUJeAPn_Ag,4873
1772
1773
  mindroot/coreplugins/user_service/backup/admin_service.py,sha256=scc59rxlZz4uuVvgjf-9HL2gKi7-uiCdSt6LjWJILR8,4259
1773
1774
  mindroot/coreplugins/user_service/backup/admin_setup.py,sha256=JGszAw8nVtnNiisSUGu9jtoStKGyN44KpbRlKAhDJho,3001
1774
1775
  mindroot/coreplugins/user_service/templates/reset_password.jinja2,sha256=GE2rHNmSUlAS5EoJuu9g3KsUel5RNMKMVYTfxhi2IPM,2097
@@ -1804,7 +1805,7 @@ mindroot/lib/plugins/__init__.py,sha256=ZpIgOKsGjyvAe9KenqZ_5ppxaQ7br9EmU7RBI_VX
1804
1805
  mindroot/lib/plugins/default_plugin_manifest.json,sha256=lg3mIQmQPU6nkOKjClstvXGC82tNP9RuV-LLQVIJAIw,1406
1805
1806
  mindroot/lib/plugins/installation.py,sha256=Ju3gD3cv9I5qIY0hrL7-4t5xYeivEtEgUveWIiE1oqM,15360
1806
1807
  mindroot/lib/plugins/loader.py,sha256=OhBzfzHuZCwxVtloVrgkHUJCaLG3_n4k9lDLMIuuZsg,9541
1807
- mindroot/lib/plugins/manifest.py,sha256=DOCpwZRFCbk4fAhXlh67uDunYGMBD-OK6_W4E9tf-_k,3955
1808
+ mindroot/lib/plugins/manifest.py,sha256=33jnEWI-2eNmWRXHVxdQWPovLSiyeD_gTIsna9a8u-M,4474
1808
1809
  mindroot/lib/plugins/mapping.py,sha256=B3nxbStLzyYj8CEuEfxWXMbCu_dOtK_zVTbcCBw5mMc,838
1809
1810
  mindroot/lib/plugins/paths.py,sha256=vPDjEF9NU1xHMIcnN4_Hf1gjyleyXAAfRcf86WXt4M4,4312
1810
1811
  mindroot/lib/providers/__init__.py,sha256=e0DzrmFHxZJCzWf-GE41_n6zjzN1XmL_d-8cBcmYh5U,11681
@@ -1826,9 +1827,9 @@ mindroot/protocols/services/stream_chat.py,sha256=fMnPfwaB5fdNMBLTEg8BXKAGvrELKH
1826
1827
  mindroot/registry/__init__.py,sha256=40Xy9bmPHsgdIrOzbtBGzf4XMqXVi9P8oZTJhn0r654,151
1827
1828
  mindroot/registry/component_manager.py,sha256=WZFNPg4SNvpqsM5NFiC2DpgmrJQCyR9cNhrCBpp30Qk,995
1828
1829
  mindroot/registry/data_access.py,sha256=NgNMamxIjaKeYxzxnVaQz1Y-Rm0AI51si3788_JHUTM,5316
1829
- mindroot-9.2.0.dist-info/licenses/LICENSE,sha256=8plAmZh8y9ccuuqFFz4kp7G-cO_qsPgAOoHNvabSB4U,1070
1830
- mindroot-9.2.0.dist-info/METADATA,sha256=YMYUpTxmYkwH8qigO998NI6gc_R0H5qQVelCoz_-UGs,891
1831
- mindroot-9.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
1832
- mindroot-9.2.0.dist-info/entry_points.txt,sha256=0bpyjMccLttx6VcjDp6zfJPN0Kk0rffor6IdIbP0j4c,50
1833
- mindroot-9.2.0.dist-info/top_level.txt,sha256=gwKm7DmNjhdrCJTYCrxa9Szne4lLpCtrEBltfsX-Mm8,9
1834
- mindroot-9.2.0.dist-info/RECORD,,
1830
+ mindroot-9.3.0.dist-info/licenses/LICENSE,sha256=8plAmZh8y9ccuuqFFz4kp7G-cO_qsPgAOoHNvabSB4U,1070
1831
+ mindroot-9.3.0.dist-info/METADATA,sha256=XifktWZGe9gF5apBJpZIgxLKNikiSXIOPqrGsMmTcus,891
1832
+ mindroot-9.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
1833
+ mindroot-9.3.0.dist-info/entry_points.txt,sha256=0bpyjMccLttx6VcjDp6zfJPN0Kk0rffor6IdIbP0j4c,50
1834
+ mindroot-9.3.0.dist-info/top_level.txt,sha256=gwKm7DmNjhdrCJTYCrxa9Szne4lLpCtrEBltfsX-Mm8,9
1835
+ mindroot-9.3.0.dist-info/RECORD,,