mindroot 9.2.0__py3-none-any.whl → 9.5.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.
Files changed (186) hide show
  1. mindroot/coreplugins/admin/__init__.py +3 -1
  2. mindroot/coreplugins/admin/agent_router.py +250 -7
  3. mindroot/coreplugins/admin/asset_manager.py +164 -0
  4. mindroot/coreplugins/admin/command_router.py +236 -1
  5. mindroot/coreplugins/admin/mcp_catalog_routes.py +156 -0
  6. mindroot/coreplugins/admin/mcp_publish_routes.py +450 -0
  7. mindroot/coreplugins/admin/mcp_registry_routes.py +495 -0
  8. mindroot/coreplugins/admin/mcp_routes.py +216 -0
  9. mindroot/coreplugins/admin/mod.py +62 -0
  10. mindroot/coreplugins/admin/oauth_callback_router.py +84 -0
  11. mindroot/coreplugins/admin/persona_handler.py +15 -6
  12. mindroot/coreplugins/admin/persona_router.py +158 -2
  13. mindroot/coreplugins/admin/plugin_manager.py +63 -0
  14. mindroot/coreplugins/admin/plugin_router.py +1 -1
  15. mindroot/coreplugins/admin/plugin_router_fixed.py +23 -0
  16. mindroot/coreplugins/admin/plugin_router_new_not_working.py +145 -0
  17. mindroot/coreplugins/admin/plugin_routes.py +114 -0
  18. mindroot/coreplugins/admin/registry_settings_routes.py +140 -0
  19. mindroot/coreplugins/admin/router.py +116 -15
  20. mindroot/coreplugins/admin/service_models.py +1 -1
  21. mindroot/coreplugins/admin/settings_router.py +1 -0
  22. mindroot/coreplugins/admin/static/css/admin-custom.css +357 -2
  23. mindroot/coreplugins/admin/static/css/dark.css +1 -0
  24. mindroot/coreplugins/admin/static/css/default.css +4 -0
  25. mindroot/coreplugins/admin/static/js/about-info.js +367 -0
  26. mindroot/coreplugins/admin/static/js/agent-form.js +83 -3
  27. mindroot/coreplugins/admin/static/js/api-key-script.js +307 -0
  28. mindroot/coreplugins/admin/static/js/mcp-manager.js +348 -0
  29. mindroot/coreplugins/admin/static/js/mcp-publisher.js +780 -0
  30. mindroot/coreplugins/admin/static/js/persona-editor.js +34 -5
  31. mindroot/coreplugins/admin/static/js/plugin-toggle.js +1 -1
  32. mindroot/coreplugins/admin/static/js/recommended-plugin-install.js +63 -0
  33. mindroot/coreplugins/admin/static/js/registry-auth-section.js +132 -0
  34. mindroot/coreplugins/admin/static/js/registry-manager-base.js +613 -0
  35. mindroot/coreplugins/admin/static/js/registry-manager-old.js +385 -0
  36. mindroot/coreplugins/admin/static/js/registry-manager-publish-old-delete.js +166 -0
  37. mindroot/coreplugins/admin/static/js/registry-manager.js +351 -0
  38. mindroot/coreplugins/admin/static/js/registry-publish-section.js +377 -0
  39. mindroot/coreplugins/admin/static/js/registry-search-section.js +400 -0
  40. mindroot/coreplugins/admin/static/js/registry-search-section.js.bak +3 -0
  41. mindroot/coreplugins/admin/static/js/registry-settings.js +69 -0
  42. mindroot/coreplugins/admin/static/js/registry-shared-services.js +857 -0
  43. mindroot/coreplugins/admin/static/js/registry-simple-sections.js +85 -0
  44. mindroot/coreplugins/admin/static/js/secure-widget-manager.js +438 -0
  45. mindroot/coreplugins/admin/static/logo.png +0 -0
  46. mindroot/coreplugins/admin/templates/admin.jinja2 +275 -110
  47. mindroot/coreplugins/agent/Assistant/agent.json +27 -11
  48. mindroot/coreplugins/agent/agent.py +2 -2
  49. mindroot/coreplugins/agent/command_parser.py +25 -10
  50. mindroot/coreplugins/agent/templates/system.jinja2 +0 -12
  51. mindroot/coreplugins/chat/__init__.py +4 -1
  52. mindroot/coreplugins/chat/router.py +132 -20
  53. mindroot/coreplugins/chat/router_dedup_patch.py +20 -0
  54. mindroot/coreplugins/chat/services.py +31 -1
  55. mindroot/coreplugins/chat/static/css/action-fix.css +32 -0
  56. mindroot/coreplugins/chat/static/css/admin-custom.css +5 -3
  57. mindroot/coreplugins/chat/static/css/dark.css +24 -3
  58. mindroot/coreplugins/chat/static/css/default.css +24 -3
  59. mindroot/coreplugins/chat/static/css/main.css +1 -0
  60. mindroot/coreplugins/chat/static/js/action.js +137 -60
  61. mindroot/coreplugins/chat/static/js/chat-history.js +3 -0
  62. mindroot/coreplugins/chat/static/js/chat.js +59 -16
  63. mindroot/coreplugins/chat/static/js/chat.js.diff +221 -0
  64. mindroot/coreplugins/chat/static/js/chatform.js +2 -2
  65. mindroot/coreplugins/chat/static/site.webmanifest +1 -1
  66. mindroot/coreplugins/chat/templates/chat.jinja2 +3 -3
  67. mindroot/coreplugins/chat/widget_manager.py +139 -0
  68. mindroot/coreplugins/chat/widget_routes.py +287 -0
  69. mindroot/coreplugins/check_list/inject/admin.jinja2 +1 -1
  70. mindroot/coreplugins/email/__init__.py +2 -0
  71. mindroot/coreplugins/email/email_provider.py +2 -2
  72. mindroot/coreplugins/email/mod.py +100 -0
  73. mindroot/coreplugins/email/services.py +5 -3
  74. mindroot/coreplugins/email/smtp_handler.py +9 -3
  75. mindroot/coreplugins/email/test_email_service.py +75 -0
  76. mindroot/coreplugins/env_manager/mod.py +61 -25
  77. mindroot/coreplugins/home/router.py +37 -2
  78. mindroot/coreplugins/home/static/imgs/logo.png +0 -0
  79. mindroot/coreplugins/home/static/imgs/logo.png.bak +0 -0
  80. mindroot/coreplugins/home/static/imgs/logo_teal.png +0 -0
  81. mindroot/coreplugins/home/static/imgs/logo_teal2.png +0 -0
  82. mindroot/coreplugins/home/static/imgs/logo_teal_detailed.png +0 -0
  83. mindroot/coreplugins/home/static/imgs/logo_teal_python.png +0 -0
  84. mindroot/coreplugins/home/templates/home.jinja2 +15 -6
  85. mindroot/coreplugins/index/handlers/plugin_ops.py +1 -1
  86. mindroot/coreplugins/index/indices/default/index.json +6 -6
  87. mindroot/coreplugins/jwt_auth/middleware.py +47 -1
  88. mindroot/coreplugins/jwt_auth/mod.py +40 -17
  89. mindroot/coreplugins/l8n/__init__.py +6 -0
  90. mindroot/coreplugins/l8n/debug_loader.py +85 -0
  91. mindroot/coreplugins/l8n/debug_middleware.py +74 -0
  92. mindroot/coreplugins/l8n/l8n_constants.py +19 -0
  93. mindroot/coreplugins/l8n/language_detection.py +183 -0
  94. mindroot/coreplugins/l8n/middleware.py +151 -0
  95. mindroot/coreplugins/l8n/mod.py +277 -0
  96. mindroot/coreplugins/l8n/monkey_patch_to_delete.py +186 -0
  97. mindroot/coreplugins/l8n/test_enhanced.py +298 -0
  98. mindroot/coreplugins/l8n/test_l8n.py +95 -0
  99. mindroot/coreplugins/l8n/test_l8n_standalone.py +251 -0
  100. mindroot/coreplugins/l8n/test_middleware.py +272 -0
  101. mindroot/coreplugins/l8n/utils.py +232 -0
  102. mindroot/coreplugins/mcp_/__init__.py +14 -0
  103. mindroot/coreplugins/mcp_/catalog_commands.py +328 -0
  104. mindroot/coreplugins/mcp_/catalog_manager.py +263 -0
  105. mindroot/coreplugins/mcp_/dynamic_commands.py +154 -0
  106. mindroot/coreplugins/mcp_/mcp_manager.py +1031 -0
  107. mindroot/coreplugins/mcp_/mod.py +367 -0
  108. mindroot/coreplugins/mcp_/oauth_storage.py +144 -0
  109. mindroot/coreplugins/mcp_/server_installer.py +79 -0
  110. mindroot/coreplugins/mcp_/setup.py +26 -0
  111. mindroot/coreplugins/mcp_/test_dynamic_commands.py +134 -0
  112. mindroot/coreplugins/mcp_/testmcpclient.py +92 -0
  113. mindroot/coreplugins/persona/mod.py +12 -7
  114. mindroot/coreplugins/signup/templates/signup.jinja2 +1 -1
  115. mindroot/coreplugins/subscriptions/__init__.py +1 -0
  116. mindroot/coreplugins/subscriptions/mod.py +14 -3
  117. mindroot/coreplugins/subscriptions/router.py +3 -0
  118. mindroot/coreplugins/user_service/__init__.py +1 -2
  119. mindroot/coreplugins/user_service/admin_init.py +1 -0
  120. mindroot/coreplugins/user_service/email_service.py +72 -17
  121. mindroot/coreplugins/user_service/mod.py +10 -2
  122. mindroot/coreplugins/user_service/password_reset_service.py +180 -27
  123. mindroot/coreplugins/user_service/router.py +84 -22
  124. mindroot/lib/auth/api_key.py +28 -0
  125. mindroot/lib/cli/plugins.py +94 -0
  126. mindroot/lib/plugins/default_plugin_manifest.json +20 -0
  127. mindroot/lib/plugins/installation.py +5 -5
  128. mindroot/lib/plugins/l8n_static_handler.py +225 -0
  129. mindroot/lib/plugins/loader.py +33 -3
  130. mindroot/lib/plugins/loader_with_l8n.py +281 -0
  131. mindroot/lib/plugins/manifest.py +238 -17
  132. mindroot/lib/providers/commands.py +3 -1
  133. mindroot/lib/route_decorators.py +5 -5
  134. mindroot/lib/templates.py +183 -11
  135. mindroot/lib/utils/merge_arrays.py +1 -1
  136. mindroot/migrate.py +49 -0
  137. mindroot/registry/data_access.py +1 -1
  138. mindroot/server.py +47 -13
  139. mindroot/server_missing_normal_args.py +197 -0
  140. mindroot/server_prev.py +173 -0
  141. {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/METADATA +7 -2
  142. {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/RECORD +147 -114
  143. mindroot/coreplugins/admin/static/favicon/about.txt +0 -6
  144. mindroot/coreplugins/admin/static/favicon/android-chrome-512x512.png +0 -0
  145. mindroot/coreplugins/admin/static/favicon/apple-touch-icon.png +0 -0
  146. mindroot/coreplugins/admin/static/favicon/favicon-16x16.png +0 -0
  147. mindroot/coreplugins/admin/static/favicon/favicon-32x32.png +0 -0
  148. mindroot/coreplugins/admin/static/favicon/favicon.ico +0 -0
  149. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/about.txt +0 -6
  150. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/android-chrome-192x192.png +0 -0
  151. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/android-chrome-512x512.png +0 -0
  152. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/apple-touch-icon.png +0 -0
  153. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon-16x16.png +0 -0
  154. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon-32x32.png +0 -0
  155. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon.ico +0 -0
  156. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/site.webmanifest +0 -1
  157. mindroot/coreplugins/admin/static/favicon/logo.png +0 -0
  158. mindroot/coreplugins/admin/static/favicon/site.webmanifest +0 -1
  159. mindroot/coreplugins/admin/static/js/backup/agent-editor.js +0 -186
  160. mindroot/coreplugins/admin/static/js/backup/agent-form.js +0 -1133
  161. mindroot/coreplugins/admin/static/js/backup/agent-list.js +0 -94
  162. mindroot/coreplugins/chat/static/favicon/about.txt +0 -6
  163. mindroot/coreplugins/chat/static/favicon/android-chrome-192x192.png +0 -0
  164. mindroot/coreplugins/chat/static/favicon/android-chrome-512x512.png +0 -0
  165. mindroot/coreplugins/chat/static/favicon/apple-touch-icon.png +0 -0
  166. mindroot/coreplugins/chat/static/favicon/favicon-16x16.png +0 -0
  167. mindroot/coreplugins/chat/static/favicon/favicon-32x32.png +0 -0
  168. mindroot/coreplugins/chat/static/favicon/favicon.ico +0 -0
  169. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/about.txt +0 -6
  170. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/android-chrome-192x192.png +0 -0
  171. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/android-chrome-512x512.png +0 -0
  172. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/apple-touch-icon.png +0 -0
  173. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon-16x16.png +0 -0
  174. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon-32x32.png +0 -0
  175. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon.ico +0 -0
  176. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/site.webmanifest +0 -1
  177. mindroot/coreplugins/chat/static/favicon/logo.png +0 -0
  178. mindroot/coreplugins/chat/static/favicon/site.webmanifest +0 -1
  179. mindroot/coreplugins/index/default.json +0 -76
  180. mindroot/coreplugins/user_service/file_trigger_service.py +0 -72
  181. mindroot/coreplugins/user_service/hooks.py +0 -23
  182. /mindroot/coreplugins/{admin/static/favicon/android-chrome-192x192.png → home/static/imgs/backuplogo.png} +0 -0
  183. {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/WHEEL +0 -0
  184. {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/entry_points.txt +0 -0
  185. {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/licenses/LICENSE +0 -0
  186. {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,216 @@
1
+ from fastapi import APIRouter, HTTPException
2
+ import os
3
+ from pydantic import BaseModel
4
+ from typing import Optional, List, Dict, Any
5
+ from lib.route_decorators import requires_role
6
+
7
+ # Import MCP components - prefer new mcp_ module, fallback to legacy if needed
8
+ try:
9
+ from mindroot.coreplugins.mcp_.mod import mcp_manager, MCPServer
10
+ except ImportError:
11
+ try:
12
+ from mindroot.coreplugins.mcp.mod import mcp_manager, MCPServer
13
+ except ImportError:
14
+ # Mock objects if MCP plugin is not fully installed, to prevent startup crash
15
+ mcp_manager = None
16
+ MCPServer = None
17
+
18
+ # Create router with admin role requirement
19
+ router = APIRouter(
20
+ dependencies=[requires_role('admin')]
21
+ )
22
+
23
+ class McpLocalTestRequest(BaseModel):
24
+ name: str
25
+ command: str
26
+ args: List[str] = []
27
+ env: Dict[str, str] = {}
28
+ secrets: Optional[Dict[str, str]] = None
29
+
30
+ class McpServerRequest(BaseModel):
31
+ server_name: str
32
+
33
+ class McpConnectRequest(BaseModel):
34
+ server_name: str
35
+ secrets: Optional[Dict[str, str]] = None
36
+
37
+ class McpServerAddRequest(BaseModel):
38
+ name: str
39
+ description: str
40
+ command: Optional[str] = None
41
+ args: List[str] = []
42
+ env: dict = {}
43
+ transport: str = "stdio"
44
+ url: Optional[str] = None
45
+ # New URL fields
46
+ provider_url: Optional[str] = None
47
+ transport_url: Optional[str] = None
48
+ transport_type: Optional[str] = None # "sse" | "streamable_http"
49
+ # OAuth 2.0 fields
50
+ auth_type: str = "none"
51
+ auth_headers: Dict[str, str] = {}
52
+ authorization_server_url: Optional[str] = None
53
+ client_id: Optional[str] = None
54
+ client_secret: Optional[str] = None
55
+ scopes: List[str] = []
56
+ redirect_uri: Optional[str] = None
57
+
58
+ class McpOAuthCallbackRequest(BaseModel):
59
+ server_name: str
60
+ code: str
61
+ state: Optional[str] = None
62
+
63
+ # --- MCP Server Management Routes ---
64
+
65
+ @router.get("/mcp/list")
66
+ async def list_mcp_servers():
67
+ """List all configured MCP servers."""
68
+ if not mcp_manager:
69
+ raise HTTPException(status_code=501, detail="MCP Plugin not available")
70
+
71
+ try:
72
+ servers = []
73
+ for name, server in mcp_manager.servers.items():
74
+ servers.append({
75
+ "name": name,
76
+ "description": server.description,
77
+ "status": server.status,
78
+ "transport": server.transport,
79
+ "command": server.command,
80
+ "args": server.args,
81
+ "capabilities": server.capabilities
82
+ })
83
+ return {"success": True, "data": servers}
84
+ except Exception as e:
85
+ raise HTTPException(status_code=500, detail=str(e))
86
+
87
+ @router.post("/mcp/add")
88
+ async def add_mcp_server(server_request: McpServerAddRequest):
89
+ """Add a new MCP server configuration."""
90
+ if not mcp_manager:
91
+ raise HTTPException(status_code=501, detail="MCP Plugin not available")
92
+
93
+ try:
94
+ # Create MCPServer object
95
+ # Validate required fields based on transport type
96
+ if server_request.transport == "stdio" and not server_request.command:
97
+ raise HTTPException(status_code=400, detail="Command is required for stdio transport")
98
+
99
+ if server_request.transport in ["http", "sse", "websocket"] and not server_request.url:
100
+ raise HTTPException(status_code=400, detail="URL is required for remote transports")
101
+
102
+ # More flexible OAuth validation - allow registry servers to be added without client_id initially
103
+ # The OAuth configuration might be provided by the registry or discovered during connection
104
+ if server_request.auth_type == "oauth2":
105
+ # Log the OAuth configuration for debugging
106
+ print(f"DEBUG: Adding OAuth server '{server_request.name}'")
107
+ print(f"DEBUG: client_id: {server_request.client_id}")
108
+ print(f"DEBUG: authorization_server_url: {server_request.authorization_server_url}")
109
+ print(f"DEBUG: scopes: {server_request.scopes}")
110
+ # Note: client_id validation removed to allow registry servers with dynamic OAuth discovery
111
+
112
+ server = MCPServer(
113
+ name=server_request.name,
114
+ description=server_request.description,
115
+ command=server_request.command or "",
116
+ args=server_request.args,
117
+ env=server_request.env,
118
+ transport=server_request.transport,
119
+ url=server_request.url,
120
+ auth_type=server_request.auth_type,
121
+ auth_headers=server_request.auth_headers,
122
+ authorization_server_url=server_request.authorization_server_url,
123
+ client_id=server_request.client_id,
124
+ client_secret=server_request.client_secret,
125
+ scopes=server_request.scopes,
126
+ redirect_uri=server_request.redirect_uri or f"{server_request.url}/oauth/callback" if server_request.url else None
127
+ )
128
+
129
+ # Use BASE_URL for redirect_uri if not explicitly provided
130
+ if not server.redirect_uri and server_request.auth_type == "oauth2":
131
+ base_url = os.getenv('BASE_URL', 'http://localhost:3000')
132
+ server.redirect_uri = f"{base_url}/mcp_oauth_cb"
133
+
134
+ mcp_manager.add_server(server_request.name, server)
135
+
136
+ return {
137
+ "success": True,
138
+ "message": f"MCP server '{server_request.name}' added successfully."
139
+ }
140
+ except Exception as e:
141
+ raise HTTPException(status_code=500, detail=str(e))
142
+
143
+ @router.post("/mcp/remove")
144
+ async def remove_mcp_server(request: McpServerRequest):
145
+ """Remove an MCP server configuration."""
146
+ if not mcp_manager:
147
+ raise HTTPException(status_code=501, detail="MCP Plugin not available")
148
+
149
+ try:
150
+ # First disconnect if connected
151
+ if request.server_name in mcp_manager.sessions:
152
+ await mcp_manager.disconnect_server(request.server_name)
153
+
154
+ # Remove server configuration
155
+ mcp_manager.remove_server(request.server_name)
156
+
157
+ return {
158
+ "success": True,
159
+ "message": f"MCP server '{request.server_name}' removed successfully."
160
+ }
161
+ except Exception as e:
162
+ raise HTTPException(status_code=500, detail=str(e))
163
+
164
+ @router.post("/mcp/connect")
165
+ async def connect_mcp_server(request: McpConnectRequest):
166
+ """Connect to an MCP server."""
167
+ if not mcp_manager:
168
+ raise HTTPException(status_code=501, detail="MCP Plugin not available")
169
+
170
+ # Persist secrets if provided
171
+ if request.secrets:
172
+ if request.server_name in mcp_manager.servers:
173
+ server = mcp_manager.servers[request.server_name]
174
+ if server.secrets is None:
175
+ server.secrets = {}
176
+ server.secrets.update(request.secrets)
177
+ # This will save the updated server config, including secrets
178
+ mcp_manager.save_config()
179
+
180
+ try:
181
+ success = await mcp_manager.connect_server(request.server_name, secrets=request.secrets)
182
+ if success:
183
+ return {
184
+ "success": True,
185
+ "message": f"MCP server '{request.server_name}' connected successfully."
186
+ }
187
+ else:
188
+ print(f"DEBUG: Failed to connect to MCP server '{request.server_name}'")
189
+ raise HTTPException(
190
+ status_code=500,
191
+ detail=f"Failed to connect to MCP server '{request.server_name}'. Check logs for details."
192
+ )
193
+ except Exception as e:
194
+ raise HTTPException(status_code=500, detail=str(e))
195
+
196
+
197
+ @router.post("/mcp/disconnect")
198
+ async def disconnect_mcp_server(request: McpServerRequest):
199
+ """Disconnect from an MCP server."""
200
+ if not mcp_manager:
201
+ raise HTTPException(status_code=501, detail="MCP Plugin not available")
202
+
203
+ try:
204
+ success = await mcp_manager.disconnect_server(request.server_name)
205
+ if success:
206
+ return {
207
+ "success": True,
208
+ "message": f"MCP server '{request.server_name}' disconnected successfully."
209
+ }
210
+ else:
211
+ raise HTTPException(
212
+ status_code=500,
213
+ detail=f"Failed to disconnect from MCP server '{request.server_name}'."
214
+ )
215
+ except Exception as e:
216
+ raise HTTPException(status_code=500, detail=str(e))
@@ -0,0 +1,62 @@
1
+ import os
2
+ import subprocess
3
+ from datetime import datetime
4
+
5
+ async def get_git_version_info(context=None):
6
+ """Get git commit hash and date of last commit.
7
+
8
+ Returns a dictionary with commit hash and date, or None if not in a git repo.
9
+
10
+ Example:
11
+ { "get_git_version_info": {} }
12
+ """
13
+ try:
14
+ # Get the current working directory or use a default path
15
+ repo_path = os.getcwd()
16
+ if '/files/mindroot' in repo_path or repo_path.endswith('mindroot'):
17
+ # We're likely in the right place
18
+ pass
19
+ else:
20
+ # Try to find mindroot directory
21
+ if os.path.exists('/files/mindroot'):
22
+ repo_path = '/files/mindroot'
23
+ else:
24
+ return None
25
+
26
+ # Get commit hash
27
+ result = subprocess.run(
28
+ ['git', 'rev-parse', 'HEAD'],
29
+ cwd=repo_path,
30
+ capture_output=True,
31
+ text=True,
32
+ timeout=10
33
+ )
34
+
35
+ if result.returncode != 0:
36
+ return None
37
+
38
+ commit_hash = result.stdout.strip()
39
+
40
+ # Get commit date
41
+ result = subprocess.run(
42
+ ['git', 'log', '-1', '--format=%ci'],
43
+ cwd=repo_path,
44
+ capture_output=True,
45
+ text=True,
46
+ timeout=10
47
+ )
48
+
49
+ if result.returncode != 0:
50
+ return None
51
+
52
+ commit_date = result.stdout.strip()
53
+
54
+ return {
55
+ 'commit_hash': commit_hash,
56
+ 'commit_date': commit_date,
57
+ 'retrieved_at': datetime.now().isoformat()
58
+ }
59
+
60
+ except Exception as e:
61
+ return None
62
+
@@ -0,0 +1,84 @@
1
+ """OAuth callback router for MCP servers.
2
+
3
+ This router contains public routes that do not require authentication,
4
+ as external OAuth providers need to be able to redirect to these endpoints.
5
+ """
6
+
7
+ from fastapi import APIRouter, Request
8
+ from fastapi.responses import HTMLResponse
9
+ from lib.route_decorators import public_route
10
+
11
+ # Create router without dependencies - routes will be public
12
+ router = APIRouter()
13
+
14
+ @router.get("/mcp_oauth_cb")
15
+ @public_route()
16
+ async def mcp_oauth_callback(request: Request):
17
+ """Handle OAuth callback for MCP servers.
18
+
19
+ This endpoint must be publicly accessible as external OAuth providers
20
+ will redirect to it without any authentication.
21
+ """
22
+ try:
23
+ # Get query parameters
24
+ code = request.query_params.get('code')
25
+ state = request.query_params.get('state')
26
+ error = request.query_params.get('error')
27
+
28
+ if error:
29
+ # OAuth error occurred - avoid f-string issues
30
+ error_html = (
31
+ "<html><body>"
32
+ "<h2>OAuth Authorization Failed</h2>"
33
+ f"<p>Error: {error}</p>"
34
+ "<p>You can close this window.</p>"
35
+ "<script>window.close();</script>"
36
+ "</body></html>"
37
+ )
38
+ return HTMLResponse(error_html)
39
+
40
+ if code:
41
+ # Success - show completion page
42
+ state_value = state or ""
43
+ # Build HTML without f-string to avoid escaping issues
44
+ success_html = (
45
+ "<html><body>"
46
+ "<h2>OAuth Authorization Successful</h2>"
47
+ "<p>Authorization code received. You can close this window.</p>"
48
+ "<script>"
49
+ "if (window.opener) {"
50
+ "window.opener.postMessage({"
51
+ "type: 'oauth_callback',"
52
+ f"code: '{code}',"
53
+ f"state: '{state_value}'"
54
+ "}, '*');"
55
+ "}"
56
+ "setTimeout(() => window.close(), 2000);"
57
+ "</script>"
58
+ "</body></html>"
59
+ )
60
+ return HTMLResponse(success_html)
61
+
62
+ # No code or error - invalid callback
63
+ invalid_html = (
64
+ "<html><body>"
65
+ "<h2>Invalid OAuth Callback</h2>"
66
+ "<p>Missing authorization code or error parameter.</p>"
67
+ "<p>You can close this window.</p>"
68
+ "<script>window.close();</script>"
69
+ "</body></html>"
70
+ )
71
+ return HTMLResponse(invalid_html)
72
+
73
+ except Exception as e:
74
+ # Error handling - escape special characters
75
+ error_message = str(e).replace('"', '&quot;').replace("'", "&#39;")
76
+ error_html = (
77
+ "<html><body>"
78
+ "<h2>OAuth Callback Error</h2>"
79
+ f"<p>An error occurred processing the OAuth callback: {error_message}</p>"
80
+ "<p>You can close this window.</p>"
81
+ "<script>window.close();</script>"
82
+ "</body></html>"
83
+ )
84
+ return HTMLResponse(error_html)
@@ -27,7 +27,7 @@ def import_persona_from_index(index: str, persona: str):
27
27
  print("Successfully imported persona", persona, "from index", index)
28
28
 
29
29
 
30
- def handle_persona_import(persona_data: dict, scope: str) -> str:
30
+ def handle_persona_import(persona_data: dict, scope: str, owner: str = None) -> str:
31
31
  """Handle importing a persona from embedded data in agent configuration.
32
32
  Returns the persona name to be used in agent configuration.
33
33
 
@@ -57,13 +57,22 @@ def handle_persona_import(persona_data: dict, scope: str) -> str:
57
57
  detail='Persona name required in persona data'
58
58
  )
59
59
 
60
- # Create persona path
61
- persona_path = Path('personas') / scope / persona_name / 'persona.json'
60
+ # For registry installs, use namespaced path
61
+ if owner and scope == 'registry':
62
+ persona_path = Path(f'personas/registry/{owner}/{persona_name}/persona.json')
63
+ return_name = f'registry/{owner}/{persona_name}'
64
+ else:
65
+ persona_path = Path('personas') / scope / persona_name / 'persona.json'
66
+ return_name = persona_name
62
67
 
63
68
  # Check if persona already exists
64
69
  if persona_path.exists():
65
- logger.warning(f"Persona '{persona_name}' already exists in {scope} scope - skipping import")
66
- return persona_name
70
+ # For registry personas with owner, we should allow overwrite to support updates
71
+ if owner and scope == 'registry':
72
+ logger.info(f"Overwriting existing registry persona '{persona_name}' for owner '{owner}'")
73
+ else:
74
+ logger.warning(f"Persona '{persona_name}' already exists in {scope} scope - skipping import")
75
+ return return_name
67
76
 
68
77
  try:
69
78
  # Create persona directory and save data
@@ -72,7 +81,7 @@ def handle_persona_import(persona_data: dict, scope: str) -> str:
72
81
  json.dump(persona_data, f, indent=2)
73
82
 
74
83
  logger.info(f"Successfully imported persona '{persona_name}' to {scope} scope")
75
- return persona_name
84
+ return return_name
76
85
 
77
86
  except Exception as e:
78
87
  logger.error(f"Failed to import persona '{persona_name}': {str(e)}")
@@ -1,8 +1,10 @@
1
1
  from fastapi import APIRouter, HTTPException, UploadFile, File, Form
2
+ from fastapi.responses import Response
2
3
  from pathlib import Path
3
4
  import json
4
5
  import shutil
5
6
  import traceback
7
+ from .asset_manager import asset_manager
6
8
 
7
9
  router = APIRouter()
8
10
 
@@ -12,9 +14,13 @@ shared_dir = BASE_DIR / "shared"
12
14
  local_dir.mkdir(parents=True, exist_ok=True)
13
15
  shared_dir.mkdir(parents=True, exist_ok=True)
14
16
 
17
+ # Registry directory for namespaced personas
18
+ registry_dir = BASE_DIR / "registry"
19
+ registry_dir.mkdir(parents=True, exist_ok=True)
20
+
15
21
  @router.get('/personas/{scope}/{name}')
16
22
  def read_persona(scope: str, name: str):
17
- if scope not in ['local', 'shared']:
23
+ if scope not in ['local', 'shared', 'registry']:
18
24
  raise HTTPException(status_code=400, detail='Invalid scope')
19
25
  persona_path = BASE_DIR / scope / name / 'persona.json'
20
26
  if not persona_path.exists():
@@ -28,13 +34,163 @@ def read_persona(scope: str, name: str):
28
34
 
29
35
  @router.get('/personas/{scope}')
30
36
  def list_personas(scope: str):
31
- if scope not in ['local', 'shared']:
37
+ if scope not in ['local', 'shared', 'registry']:
32
38
  raise HTTPException(status_code=400, detail='Invalid scope')
33
39
  scope_dir = BASE_DIR / scope
34
40
  personas = [p.name for p in scope_dir.iterdir() if p.is_dir()]
35
41
  print(f"Read personas from dir {scope_dir}: {personas}")
36
42
  return [{'name': name} for name in personas]
37
43
 
44
+ @router.get('/personas/{persona_path:path}')
45
+ def read_persona_by_path(persona_path: str):
46
+ """Read persona by full path (supports registry/owner/name format)"""
47
+ try:
48
+ # Handle registry personas: registry/owner/name
49
+ if persona_path.startswith('registry/'):
50
+ full_path = BASE_DIR / persona_path / 'persona.json'
51
+ else:
52
+ # Handle simple names: check local first, then shared
53
+ full_path = BASE_DIR / 'local' / persona_path / 'persona.json'
54
+ if not full_path.exists():
55
+ full_path = BASE_DIR / 'shared' / persona_path / 'persona.json'
56
+
57
+ if not full_path.exists():
58
+ raise HTTPException(status_code=404, detail='Persona not found')
59
+
60
+ with open(full_path, 'r') as f:
61
+ return json.load(f)
62
+
63
+ except Exception as e:
64
+ raise HTTPException(status_code=404, detail=f'Persona not found: {str(e)}')
65
+
66
+ @router.post('/personas/registry')
67
+ def create_registry_persona(persona: str = Form(...), owner: str = Form(...)):
68
+ """Create a registry persona with owner namespace"""
69
+ try:
70
+ persona_data = json.loads(persona)
71
+ persona_name = persona_data.get('name')
72
+
73
+ if not persona_name:
74
+ raise HTTPException(status_code=400, detail='Persona name is required')
75
+ if not owner:
76
+ raise HTTPException(status_code=400, detail='Owner is required for registry personas')
77
+
78
+ # Create registry persona path: personas/registry/owner/name/
79
+ persona_path = BASE_DIR / 'registry' / owner / persona_name / 'persona.json'
80
+
81
+ if persona_path.exists():
82
+ # Update existing persona instead of failing
83
+ print(f"Updating existing registry persona: {owner}/{persona_name}")
84
+
85
+ persona_path.parent.mkdir(parents=True, exist_ok=True)
86
+
87
+ with open(persona_path, 'w') as f:
88
+ json.dump(persona_data, f, indent=2)
89
+
90
+ return {'status': 'success', 'path': f'registry/{owner}/{persona_name}'}
91
+
92
+ except Exception as e:
93
+ print(f"Error creating registry persona: {e}")
94
+ raise HTTPException(status_code=500, detail=f'Internal server error: {str(e)}')
95
+
96
+ @router.post('/personas/registry/with-assets')
97
+ def create_registry_persona_with_assets(persona: str = Form(...), owner: str = Form(...),
98
+ faceref: UploadFile = File(None), avatar: UploadFile = File(None)):
99
+ """Create a registry persona with deduplicated asset storage"""
100
+ try:
101
+ print(f"Received persona data (first 200 chars): {persona[:200]}...")
102
+ print(f"Received owner: {owner}")
103
+ print(f"Received faceref: {faceref.filename if faceref else 'None'}")
104
+ print(f"Received avatar: {avatar.filename if avatar else 'None'}")
105
+
106
+ persona_data = json.loads(persona)
107
+ persona_name = persona_data.get('name')
108
+
109
+ if not persona_name:
110
+ raise HTTPException(status_code=400, detail='Persona name is required')
111
+ if not owner:
112
+ raise HTTPException(status_code=400, detail='Owner is required for registry personas')
113
+
114
+ # Store assets with deduplication
115
+ asset_hashes = {}
116
+
117
+ if faceref:
118
+ content = faceref.file.read()
119
+ file_hash, was_new = asset_manager.store_content(content, faceref.filename, "faceref")
120
+ asset_hashes['faceref'] = file_hash
121
+
122
+ if avatar:
123
+ content = avatar.file.read()
124
+ file_hash, was_new = asset_manager.store_content(content, avatar.filename, "avatar")
125
+ asset_hashes['avatar'] = file_hash
126
+
127
+ # Add asset references to persona data
128
+ persona_data['asset_hashes'] = asset_hashes
129
+
130
+ # For registry personas, update the name to include the owner namespace
131
+ # This ensures the chat UI can find the images at the correct path
132
+ if 'name' in persona_data:
133
+ persona_data['name'] = f"{owner}/{persona_name}"
134
+
135
+ # Create registry persona path first
136
+ persona_path = BASE_DIR / 'registry' / owner / persona_name / 'persona.json'
137
+ persona_path.parent.mkdir(parents=True, exist_ok=True)
138
+
139
+ # ALSO copy assets to traditional locations for compatibility
140
+ if faceref:
141
+ faceref_path = persona_path.parent / 'faceref.png'
142
+ faceref_path.parent.mkdir(parents=True, exist_ok=True)
143
+ with open(faceref_path, 'wb') as f:
144
+ faceref.file.seek(0) # Reset file pointer
145
+ f.write(faceref.file.read())
146
+
147
+ if avatar:
148
+ avatar_path = persona_path.parent / 'avatar.png'
149
+ avatar_path.parent.mkdir(parents=True, exist_ok=True)
150
+ with open(avatar_path, 'wb') as f:
151
+ avatar.file.seek(0) # Reset file pointer
152
+ f.write(avatar.file.read())
153
+
154
+ with open(persona_path, 'w') as f:
155
+ json.dump(persona_data, f, indent=2)
156
+
157
+ return {
158
+ 'status': 'success',
159
+ 'path': f'registry/{owner}/{persona_name}',
160
+ 'asset_hashes': asset_hashes
161
+ }
162
+
163
+ except json.JSONDecodeError as e:
164
+ print(f"JSON decode error: {e}")
165
+ print(f"Invalid JSON received: {persona}")
166
+ raise HTTPException(status_code=400, detail=f'Invalid JSON in persona data: {str(e)}')
167
+ except Exception as e:
168
+ print(f"Error creating registry persona with assets: {e}")
169
+ import traceback
170
+ traceback.print_exc()
171
+ raise HTTPException(status_code=500, detail=f'Internal server error: {str(e)}')
172
+
173
+ @router.get('/assets/{asset_hash}')
174
+ def serve_asset(asset_hash: str):
175
+ """Serve a deduplicated asset by hash"""
176
+ try:
177
+ asset_path = asset_manager.get_asset_path(asset_hash)
178
+ if not asset_path:
179
+ raise HTTPException(status_code=404, detail='Asset not found')
180
+
181
+ metadata = asset_manager.get_asset_metadata(asset_hash)
182
+
183
+ with open(asset_path, 'rb') as f:
184
+ content = f.read()
185
+
186
+ # Determine content type based on metadata
187
+ content_type = "image/png" if metadata and metadata.get('type') in ['avatar', 'faceref'] else "application/octet-stream"
188
+
189
+ return Response(content=content, media_type=content_type)
190
+
191
+ except Exception as e:
192
+ raise HTTPException(status_code=404, detail=f'Asset not found: {str(e)}')
193
+
38
194
  @router.post('/personas/{scope}')
39
195
  def create_persona(scope: str, persona: str = Form(...), faceref: UploadFile = File(None), avatar: UploadFile = File(None)):
40
196
  try:
@@ -12,6 +12,7 @@ from lib.plugins import (
12
12
  from lib.plugins.installation import download_github_files
13
13
  from lib.streamcmd import stream_command_as_events
14
14
  import asyncio
15
+ import httpx
15
16
 
16
17
 
17
18
  router = APIRouter()
@@ -360,3 +361,65 @@ def discover_plugins(directory):
360
361
  continue
361
362
 
362
363
  return discovered
364
+
365
+ async def publish_plugin_from_github(repo: str, registry_token: str, registry_url: str):
366
+ """
367
+ Fetches plugin_info.json from a GitHub repo and publishes it to the registry.
368
+ """
369
+ plugin_info = None
370
+ # Try to fetch from 'main' and then 'master' branch
371
+ for branch in ['main', 'master']:
372
+ url = f"https://raw.githubusercontent.com/{repo}/{branch}/plugin_info.json"
373
+ async with httpx.AsyncClient() as client:
374
+ try:
375
+ response = await client.get(url)
376
+ if response.status_code == 200:
377
+ plugin_info = response.json()
378
+ break
379
+ except httpx.RequestError as e:
380
+ # This might happen if the repo is private or other network issues
381
+ print(f"Error fetching from {url}: {e}")
382
+ continue
383
+
384
+ if not plugin_info:
385
+ raise Exception(f"Could not find or access plugin_info.json in repo {repo} on 'main' or 'master' branch.")
386
+
387
+ # Construct the payload for the registry's /publish endpoint
388
+ publish_data = {
389
+ "title": plugin_info.get("name"),
390
+ "description": plugin_info.get("description", ""),
391
+ "category": "plugin",
392
+ "content_type": "mindroot_plugin",
393
+ "version": plugin_info.get("version", "0.1.0"),
394
+ "github_url": f"https://github.com/{repo}",
395
+ "pypi_module": plugin_info.get("pypi_module"),
396
+ "commands": plugin_info.get("commands", []),
397
+ "services": plugin_info.get("services", []),
398
+ "tags": plugin_info.get("tags", ["plugin"]),
399
+ "dependencies": plugin_info.get("dependencies", []),
400
+ "data": {
401
+ "plugin_info": plugin_info,
402
+ "installation": {
403
+ "type": "github",
404
+ "source_path": repo
405
+ }
406
+ }
407
+ }
408
+
409
+ # Post the data to the registry
410
+ publish_url = f"{registry_url}/publish"
411
+ headers = {
412
+ "Authorization": f"Bearer {registry_token}",
413
+ "Content-Type": "application/json"
414
+ }
415
+ async with httpx.AsyncClient() as client:
416
+ response = await client.post(publish_url, json=publish_data, headers=headers)
417
+
418
+ if response.status_code >= 400:
419
+ try:
420
+ error_detail = response.json().get("detail", response.text)
421
+ except:
422
+ error_detail = response.text
423
+ raise Exception(f"Failed to publish to registry: {response.status_code} - {error_detail}")
424
+
425
+ return response.json()
@@ -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: