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
@@ -1 +1,3 @@
1
- # This file is required to make Python treat the directory as a package.
1
+ # This file is required to make Python treat the directory as a package.
2
+ # Import commands to register them
3
+ from .mod import *
@@ -1,4 +1,5 @@
1
1
  from fastapi import APIRouter, HTTPException, UploadFile, File, Form
2
+ from fastapi.responses import StreamingResponse
2
3
  from pydantic import BaseModel
3
4
  from pathlib import Path
4
5
  import json
@@ -7,6 +8,12 @@ from lib.providers.commands import command_manager
7
8
  from .persona_handler import handle_persona_import, import_persona_from_index
8
9
  import traceback
9
10
  import hashlib
11
+ import zipfile
12
+ import io
13
+ import tempfile
14
+ import time
15
+ from typing import Dict, Any
16
+ from datetime import datetime
10
17
 
11
18
  router = APIRouter()
12
19
 
@@ -16,13 +23,88 @@ shared_dir = BASE_DIR / "shared"
16
23
  local_dir.mkdir(parents=True, exist_ok=True)
17
24
  shared_dir.mkdir(parents=True, exist_ok=True)
18
25
 
26
+ # Cache for agent ownership info
27
+ _agent_ownership_cache = None
28
+ _cache_timestamp = 0
29
+
19
30
  class GitHubImportRequest(BaseModel):
20
31
  repo_path: str # Format: "owner/repo"
21
32
  scope: str
22
33
  tag: str = None
23
34
 
35
+ def scan_agent_ownership() -> Dict[str, Any]:
36
+ """Scan all agents and build ownership information"""
37
+ ownership_info = {
38
+ 'agents': {},
39
+ 'scanned_at': datetime.now().isoformat(),
40
+ 'total_agents': 0,
41
+ 'owned_agents': 0,
42
+ 'external_agents': 0
43
+ }
44
+
45
+ # Scan both local and shared directories
46
+ for scope in ['local', 'shared']:
47
+ scope_dir = BASE_DIR / scope
48
+ if not scope_dir.exists():
49
+ continue
50
+
51
+ for agent_dir in scope_dir.iterdir():
52
+ if not agent_dir.is_dir():
53
+ continue
54
+
55
+ agent_json_path = agent_dir / 'agent.json'
56
+ if not agent_json_path.exists():
57
+ continue
58
+
59
+ try:
60
+ with open(agent_json_path, 'r') as f:
61
+ agent_data = json.load(f)
62
+
63
+ agent_name = agent_data.get('name', agent_dir.name)
64
+
65
+ # Extract ownership information
66
+ creator = agent_data.get('creator')
67
+ owner = agent_data.get('owner')
68
+ registry_owner = agent_data.get('registry_owner')
69
+ created_by = agent_data.get('created_by')
70
+
71
+ # Determine if this agent has external ownership
72
+ has_external_owner = bool(creator or owner or registry_owner or created_by)
73
+
74
+ ownership_info['agents'][f"{scope}/{agent_name}"] = {
75
+ 'name': agent_name,
76
+ 'scope': scope,
77
+ 'creator': creator,
78
+ 'owner': owner,
79
+ 'registry_owner': registry_owner,
80
+ 'created_by': created_by,
81
+ 'has_external_owner': has_external_owner,
82
+ 'description': agent_data.get('description', ''),
83
+ 'version': agent_data.get('version', '1.0.0')
84
+ }
85
+
86
+ ownership_info['total_agents'] += 1
87
+ if has_external_owner:
88
+ ownership_info['external_agents'] += 1
89
+ else:
90
+ ownership_info['owned_agents'] += 1
91
+
92
+ except Exception as e:
93
+ print(f"Error reading agent {agent_dir.name}: {e}")
94
+ continue
95
+
96
+ return ownership_info
97
+
24
98
  async def load_persona_data(persona_name: str) -> dict:
25
99
  """Load persona data from local or shared directory"""
100
+ # Handle registry personas: registry/owner/name
101
+ if persona_name.startswith('registry/'):
102
+ persona_path = Path(f'personas/{persona_name}/persona.json')
103
+ if persona_path.exists():
104
+ with open(persona_path, 'r') as f:
105
+ return json.load(f)
106
+
107
+ # Fallback to existing local/shared pattern (UNCHANGED)
26
108
  persona_path = Path('personas/local') / persona_name / 'persona.json'
27
109
  if not persona_path.exists():
28
110
  persona_path = Path('personas/shared') / persona_name / 'persona.json'
@@ -91,12 +173,55 @@ def list_agents(scope: str):
91
173
  if scope not in ['local', 'shared']:
92
174
  raise HTTPException(status_code=400, detail='Invalid scope')
93
175
  scope_dir = BASE_DIR / scope
94
- agents = [p.name for p in scope_dir.iterdir() if p.is_dir()]
95
- return [{'name': name} for name in agents]
176
+ agents = []
177
+ for p in scope_dir.iterdir():
178
+ if p.is_dir():
179
+ agent_json_path = p / 'agent.json'
180
+ if agent_json_path.exists():
181
+ try:
182
+ with open(agent_json_path, 'r') as f:
183
+ agent_data = json.load(f)
184
+ agents.append(agent_data)
185
+ except Exception as e:
186
+ # If we can't read the agent.json, just include the name
187
+ agents.append({'name': p.name})
188
+ return agents
189
+
190
+ @router.get('/agents/ownership-info')
191
+ def get_agent_ownership_info():
192
+ """Get cached ownership information for all agents"""
193
+ global _agent_ownership_cache, _cache_timestamp
194
+
195
+ # Cache for 5 minutes
196
+ cache_duration = 300 # 5 minutes in seconds
197
+ current_time = time.time()
198
+
199
+ if (_agent_ownership_cache is None or
200
+ current_time - _cache_timestamp > cache_duration):
201
+
202
+ _agent_ownership_cache = scan_agent_ownership()
203
+ _cache_timestamp = current_time
204
+
205
+ return _agent_ownership_cache
206
+
207
+ @router.post('/agents/refresh-ownership-cache')
208
+ def refresh_agent_ownership_cache():
209
+ """Force refresh of the agent ownership cache"""
210
+ global _agent_ownership_cache, _cache_timestamp
211
+
212
+ _agent_ownership_cache = scan_agent_ownership()
213
+ _cache_timestamp = time.time()
214
+
215
+ return {
216
+ 'success': True,
217
+ 'message': 'Agent ownership cache refreshed',
218
+ 'data': _agent_ownership_cache
219
+ }
96
220
 
97
221
  @router.post('/agents/{scope}')
98
222
  def create_agent(scope: str, agent: str = Form(...)):
99
223
  try:
224
+ print(f"Creating agent in scope: {scope}")
100
225
  agent_data = json.loads(agent)
101
226
  if scope not in ['local', 'shared']:
102
227
  raise HTTPException(status_code=400, detail='Invalid scope')
@@ -105,6 +230,7 @@ def create_agent(scope: str, agent: str = Form(...)):
105
230
  if not agent_name:
106
231
  raise HTTPException(status_code=400, detail='Agent name is required')
107
232
 
233
+ print(f"Agent name: {agent_name}")
108
234
  if "indexName" in agent_data and agent_data["indexName"] is not None:
109
235
  print("Import agent from index: " + agent_data["indexName"])
110
236
  import_persona_from_index(agent_data["indexName"], agent_data['persona'])
@@ -112,8 +238,12 @@ def create_agent(scope: str, agent: str = Form(...)):
112
238
  if 'persona' in agent_data:
113
239
  # This will either return the persona name or handle the import
114
240
  # and return the name
115
- persona_name = handle_persona_import(agent_data['persona'], scope)
116
-
241
+ # Extract owner information for registry personas
242
+ owner = agent_data.get('registry_owner') or agent_data.get('owner') or agent_data.get('creator')
243
+ persona_scope = 'registry' if owner else scope
244
+
245
+ persona_name = handle_persona_import(agent_data['persona'], persona_scope, owner)
246
+
117
247
  agent_data['persona'] = persona_name
118
248
 
119
249
  # Ensure recommended_plugins is present and is a list (also handle legacy required_plugins)
@@ -136,17 +266,26 @@ def create_agent(scope: str, agent: str = Form(...)):
136
266
 
137
267
  agent_path = BASE_DIR / scope / agent_name / 'agent.json'
138
268
  if agent_path.exists():
139
- raise HTTPException(status_code=400, detail='Agent already exists')
269
+ # Check if overwrite parameter is provided
270
+ overwrite = agent_data.get('overwrite', False)
271
+ if not overwrite:
272
+ raise HTTPException(status_code=400, detail='Agent already exists')
273
+ else:
274
+ # If overwrite is True, we'll proceed to overwrite the existing agent
275
+ print(f"Overwriting existing agent: {agent_name}")
140
276
 
277
+ print(f"Creating agent directory: {agent_path.parent}")
141
278
  agent_path.parent.mkdir(parents=True, exist_ok=True)
279
+ print(f"Writing agent file: {agent_path}")
142
280
  with open(agent_path, 'w') as f:
143
281
  json.dump(agent_data, f, indent=2)
144
282
 
283
+ print(f"Successfully created agent: {agent_name}")
145
284
  return {'status': 'success'}
146
285
  except Exception as e:
147
286
  trace = traceback.format_exc()
148
- raise HTTPException(status_code=500, detail='Internal server error ' + str(e) + "\n" + trace)
149
-
287
+ print(f"Error creating agent: {str(e)}\n{trace}")
288
+ raise HTTPException(status_code=500, detail=f'Internal server error: {str(e)}\nTrace: {trace}')
150
289
  @router.put('/agents/{scope}/{name}')
151
290
  def update_agent(scope: str, name: str, agent: str = Form(...)):
152
291
  try:
@@ -223,3 +362,107 @@ def import_github_agent_endpoint(request: GitHubImportRequest):
223
362
  raise HTTPException(status_code=400, detail=str(e))
224
363
  except Exception as e:
225
364
  raise HTTPException(status_code=500, detail=f"Error during GitHub import: {str(e)}")
365
+
366
+ @router.get('/agents/{scope}/{name}/export')
367
+ def export_agent_zip(scope: str, name: str):
368
+ """Export an agent as a zip file"""
369
+ if scope not in ['local', 'shared']:
370
+ raise HTTPException(status_code=400, detail='Invalid scope')
371
+
372
+ agent_dir = BASE_DIR / scope / name
373
+ if not agent_dir.exists():
374
+ raise HTTPException(status_code=404, detail='Agent not found')
375
+
376
+ try:
377
+ # Create zip file in memory
378
+ zip_buffer = io.BytesIO()
379
+
380
+ with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
381
+ # Add all files from the agent directory
382
+ for file_path in agent_dir.rglob('*'):
383
+ if file_path.is_file():
384
+ # Use relative path within the zip
385
+ arcname = file_path.relative_to(agent_dir)
386
+ zip_file.write(file_path, arcname)
387
+
388
+ zip_buffer.seek(0)
389
+
390
+ # Return as streaming response
391
+ return StreamingResponse(
392
+ io.BytesIO(zip_buffer.read()),
393
+ media_type='application/zip',
394
+ headers={'Content-Disposition': f'attachment; filename="{name}_agent.zip"'}
395
+ )
396
+
397
+ except Exception as e:
398
+ raise HTTPException(status_code=500, detail=f"Error creating zip: {str(e)}")
399
+
400
+ @router.post('/agents/{scope}/import-zip')
401
+ async def import_agent_zip(scope: str, file: UploadFile = File(...)):
402
+ """Import an agent from a zip file"""
403
+ if scope not in ['local', 'shared']:
404
+ raise HTTPException(status_code=400, detail='Invalid scope')
405
+
406
+ if not file.filename.endswith('.zip'):
407
+ raise HTTPException(status_code=400, detail='File must be a zip file')
408
+
409
+ try:
410
+ # Read the uploaded file
411
+ content = await file.read()
412
+
413
+ # Create a temporary directory for extraction
414
+ with tempfile.TemporaryDirectory() as temp_dir:
415
+ temp_path = Path(temp_dir)
416
+
417
+ # Extract zip file
418
+ with zipfile.ZipFile(io.BytesIO(content), 'r') as zip_file:
419
+ zip_file.extractall(temp_path)
420
+
421
+ # Find agent.json file
422
+ agent_json_files = list(temp_path.rglob('agent.json'))
423
+ if not agent_json_files:
424
+ raise HTTPException(status_code=400, detail='No agent.json found in zip file')
425
+
426
+ agent_json_path = agent_json_files[0]
427
+
428
+ # Load agent data
429
+ with open(agent_json_path, 'r') as f:
430
+ agent_data = json.load(f)
431
+
432
+ agent_name = agent_data.get('name')
433
+ if not agent_name:
434
+ raise HTTPException(status_code=400, detail='Agent name not found in agent.json')
435
+
436
+ # Check if agent already exists
437
+ target_dir = BASE_DIR / scope / agent_name
438
+ if target_dir.exists():
439
+ # For zip imports, we could add overwrite support in the future
440
+ # For now, we'll keep the existing behavior but make the error message consistent
441
+ raise HTTPException(
442
+ status_code=400,
443
+ detail=f'Agent {agent_name} already exists. Use the registry manager for updates.'
444
+ )
445
+
446
+ # Copy extracted files to target directory
447
+ target_dir.mkdir(parents=True, exist_ok=True)
448
+
449
+ # Get the directory containing agent.json
450
+ source_dir = agent_json_path.parent
451
+
452
+ # Copy all files from source to target
453
+ import shutil
454
+ for item in source_dir.rglob('*'):
455
+ if item.is_file():
456
+ relative_path = item.relative_to(source_dir)
457
+ target_file = target_dir / relative_path
458
+ target_file.parent.mkdir(parents=True, exist_ok=True)
459
+ shutil.copy2(item, target_file)
460
+
461
+ return {
462
+ 'success': True,
463
+ 'message': f'Agent {agent_name} imported successfully',
464
+ 'agent_name': agent_name
465
+ }
466
+
467
+ except Exception as e:
468
+ raise HTTPException(status_code=500, detail=f"Error importing zip: {str(e)}")
@@ -0,0 +1,164 @@
1
+ import hashlib
2
+ import shutil
3
+ from pathlib import Path
4
+ from typing import Dict, Optional, Tuple
5
+ import json
6
+ import os
7
+
8
+ class AssetManager:
9
+ """Manages deduplicated asset storage for personas"""
10
+
11
+ def __init__(self, base_dir: str = "registry_assets"):
12
+ self.base_dir = Path(base_dir)
13
+ self.assets_dir = self.base_dir / "assets"
14
+ self.metadata_dir = self.base_dir / "metadata"
15
+
16
+ # Create directories
17
+ self.assets_dir.mkdir(parents=True, exist_ok=True)
18
+ self.metadata_dir.mkdir(parents=True, exist_ok=True)
19
+
20
+ def calculate_file_hash(self, file_path: Path) -> str:
21
+ """Calculate SHA256 hash of a file"""
22
+ sha256_hash = hashlib.sha256()
23
+ with open(file_path, "rb") as f:
24
+ for chunk in iter(lambda: f.read(4096), b""):
25
+ sha256_hash.update(chunk)
26
+ return sha256_hash.hexdigest()
27
+
28
+ def calculate_content_hash(self, content: bytes) -> str:
29
+ """Calculate SHA256 hash of content bytes"""
30
+ return hashlib.sha256(content).hexdigest()
31
+
32
+ def store_asset(self, source_path: Path, asset_type: str = "image") -> Tuple[str, bool]:
33
+ """Store an asset and return (hash, was_new)"""
34
+ if not source_path.exists():
35
+ raise FileNotFoundError(f"Source file not found: {source_path}")
36
+
37
+ # Calculate hash
38
+ file_hash = self.calculate_file_hash(source_path)
39
+ asset_path = self.assets_dir / file_hash
40
+
41
+ # Check if asset already exists
42
+ if asset_path.exists():
43
+ # Update reference count
44
+ self._increment_reference_count(file_hash)
45
+ return file_hash, False
46
+
47
+ # Copy file to assets directory
48
+ shutil.copy2(source_path, asset_path)
49
+
50
+ # Create metadata
51
+ metadata = {
52
+ "hash": file_hash,
53
+ "type": asset_type,
54
+ "size": source_path.stat().st_size,
55
+ "original_name": source_path.name,
56
+ "reference_count": 1
57
+ }
58
+
59
+ metadata_path = self.metadata_dir / f"{file_hash}.json"
60
+ with open(metadata_path, 'w') as f:
61
+ json.dump(metadata, f, indent=2)
62
+
63
+ return file_hash, True
64
+
65
+ def store_content(self, content: bytes, filename: str, asset_type: str = "image") -> Tuple[str, bool]:
66
+ """Store content bytes and return (hash, was_new)"""
67
+ file_hash = self.calculate_content_hash(content)
68
+ asset_path = self.assets_dir / file_hash
69
+
70
+ # Check if asset already exists
71
+ if asset_path.exists():
72
+ self._increment_reference_count(file_hash)
73
+ return file_hash, False
74
+
75
+ # Write content to assets directory
76
+ with open(asset_path, 'wb') as f:
77
+ f.write(content)
78
+
79
+ # Create metadata
80
+ metadata = {
81
+ "hash": file_hash,
82
+ "type": asset_type,
83
+ "size": len(content),
84
+ "original_name": filename,
85
+ "reference_count": 1
86
+ }
87
+
88
+ metadata_path = self.metadata_dir / f"{file_hash}.json"
89
+ with open(metadata_path, 'w') as f:
90
+ json.dump(metadata, f, indent=2)
91
+
92
+ return file_hash, True
93
+
94
+ def get_asset_path(self, file_hash: str) -> Optional[Path]:
95
+ """Get the path to an asset by hash"""
96
+ asset_path = self.assets_dir / file_hash
97
+ return asset_path if asset_path.exists() else None
98
+
99
+ def get_asset_metadata(self, file_hash: str) -> Optional[Dict]:
100
+ """Get metadata for an asset"""
101
+ metadata_path = self.metadata_dir / f"{file_hash}.json"
102
+ if not metadata_path.exists():
103
+ return None
104
+
105
+ with open(metadata_path, 'r') as f:
106
+ return json.load(f)
107
+
108
+ def _increment_reference_count(self, file_hash: str):
109
+ """Increment reference count for an asset"""
110
+ metadata = self.get_asset_metadata(file_hash)
111
+ if metadata:
112
+ metadata['reference_count'] += 1
113
+ metadata_path = self.metadata_dir / f"{file_hash}.json"
114
+ with open(metadata_path, 'w') as f:
115
+ json.dump(metadata, f, indent=2)
116
+
117
+ def decrement_reference_count(self, file_hash: str) -> bool:
118
+ """Decrement reference count and delete if zero. Returns True if deleted."""
119
+ metadata = self.get_asset_metadata(file_hash)
120
+ if not metadata:
121
+ return False
122
+
123
+ metadata['reference_count'] -= 1
124
+
125
+ if metadata['reference_count'] <= 0:
126
+ # Delete asset and metadata
127
+ asset_path = self.assets_dir / file_hash
128
+ metadata_path = self.metadata_dir / f"{file_hash}.json"
129
+
130
+ if asset_path.exists():
131
+ asset_path.unlink()
132
+ if metadata_path.exists():
133
+ metadata_path.unlink()
134
+
135
+ return True
136
+ else:
137
+ # Update metadata
138
+ metadata_path = self.metadata_dir / f"{file_hash}.json"
139
+ with open(metadata_path, 'w') as f:
140
+ json.dump(metadata, f, indent=2)
141
+
142
+ return False
143
+
144
+ def get_stats(self) -> Dict:
145
+ """Get storage statistics"""
146
+ total_assets = len(list(self.assets_dir.glob('*')))
147
+ total_size = sum(f.stat().st_size for f in self.assets_dir.glob('*'))
148
+
149
+ # Calculate total references
150
+ total_references = 0
151
+ for metadata_file in self.metadata_dir.glob('*.json'):
152
+ with open(metadata_file, 'r') as f:
153
+ metadata = json.load(f)
154
+ total_references += metadata.get('reference_count', 0)
155
+
156
+ return {
157
+ 'total_assets': total_assets,
158
+ 'total_size_bytes': total_size,
159
+ 'total_references': total_references,
160
+ 'deduplication_ratio': total_references / max(total_assets, 1)
161
+ }
162
+
163
+ # Global asset manager instance
164
+ asset_manager = AssetManager()