massgen 0.0.3__py3-none-any.whl → 0.1.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 massgen might be problematic. Click here for more details.

Files changed (268) hide show
  1. massgen/__init__.py +142 -8
  2. massgen/adapters/__init__.py +29 -0
  3. massgen/adapters/ag2_adapter.py +483 -0
  4. massgen/adapters/base.py +183 -0
  5. massgen/adapters/tests/__init__.py +0 -0
  6. massgen/adapters/tests/test_ag2_adapter.py +439 -0
  7. massgen/adapters/tests/test_agent_adapter.py +128 -0
  8. massgen/adapters/utils/__init__.py +2 -0
  9. massgen/adapters/utils/ag2_utils.py +236 -0
  10. massgen/adapters/utils/tests/__init__.py +0 -0
  11. massgen/adapters/utils/tests/test_ag2_utils.py +138 -0
  12. massgen/agent_config.py +329 -55
  13. massgen/api_params_handler/__init__.py +10 -0
  14. massgen/api_params_handler/_api_params_handler_base.py +99 -0
  15. massgen/api_params_handler/_chat_completions_api_params_handler.py +176 -0
  16. massgen/api_params_handler/_claude_api_params_handler.py +113 -0
  17. massgen/api_params_handler/_response_api_params_handler.py +130 -0
  18. massgen/backend/__init__.py +39 -4
  19. massgen/backend/azure_openai.py +385 -0
  20. massgen/backend/base.py +341 -69
  21. massgen/backend/base_with_mcp.py +1102 -0
  22. massgen/backend/capabilities.py +386 -0
  23. massgen/backend/chat_completions.py +577 -130
  24. massgen/backend/claude.py +1033 -537
  25. massgen/backend/claude_code.py +1203 -0
  26. massgen/backend/cli_base.py +209 -0
  27. massgen/backend/docs/BACKEND_ARCHITECTURE.md +126 -0
  28. massgen/backend/{CLAUDE_API_RESEARCH.md → docs/CLAUDE_API_RESEARCH.md} +18 -18
  29. massgen/backend/{GEMINI_API_DOCUMENTATION.md → docs/GEMINI_API_DOCUMENTATION.md} +9 -9
  30. massgen/backend/docs/Gemini MCP Integration Analysis.md +1050 -0
  31. massgen/backend/docs/MCP_IMPLEMENTATION_CLAUDE_BACKEND.md +177 -0
  32. massgen/backend/docs/MCP_INTEGRATION_RESPONSE_BACKEND.md +352 -0
  33. massgen/backend/docs/OPENAI_GPT5_MODELS.md +211 -0
  34. massgen/backend/{OPENAI_RESPONSES_API_FORMAT.md → docs/OPENAI_RESPONSE_API_TOOL_CALLS.md} +3 -3
  35. massgen/backend/docs/OPENAI_response_streaming.md +20654 -0
  36. massgen/backend/docs/inference_backend.md +257 -0
  37. massgen/backend/docs/permissions_and_context_files.md +1085 -0
  38. massgen/backend/external.py +126 -0
  39. massgen/backend/gemini.py +1850 -241
  40. massgen/backend/grok.py +40 -156
  41. massgen/backend/inference.py +156 -0
  42. massgen/backend/lmstudio.py +171 -0
  43. massgen/backend/response.py +1095 -322
  44. massgen/chat_agent.py +131 -113
  45. massgen/cli.py +1560 -275
  46. massgen/config_builder.py +2396 -0
  47. massgen/configs/BACKEND_CONFIGURATION.md +458 -0
  48. massgen/configs/README.md +559 -216
  49. massgen/configs/ag2/ag2_case_study.yaml +27 -0
  50. massgen/configs/ag2/ag2_coder.yaml +34 -0
  51. massgen/configs/ag2/ag2_coder_case_study.yaml +36 -0
  52. massgen/configs/ag2/ag2_gemini.yaml +27 -0
  53. massgen/configs/ag2/ag2_groupchat.yaml +108 -0
  54. massgen/configs/ag2/ag2_groupchat_gpt.yaml +118 -0
  55. massgen/configs/ag2/ag2_single_agent.yaml +21 -0
  56. massgen/configs/basic/multi/fast_timeout_example.yaml +37 -0
  57. massgen/configs/basic/multi/gemini_4o_claude.yaml +31 -0
  58. massgen/configs/basic/multi/gemini_gpt5nano_claude.yaml +36 -0
  59. massgen/configs/{gemini_4o_claude.yaml → basic/multi/geminicode_4o_claude.yaml} +3 -3
  60. massgen/configs/basic/multi/geminicode_gpt5nano_claude.yaml +36 -0
  61. massgen/configs/basic/multi/glm_gemini_claude.yaml +25 -0
  62. massgen/configs/basic/multi/gpt4o_audio_generation.yaml +30 -0
  63. massgen/configs/basic/multi/gpt4o_image_generation.yaml +31 -0
  64. massgen/configs/basic/multi/gpt5nano_glm_qwen.yaml +26 -0
  65. massgen/configs/basic/multi/gpt5nano_image_understanding.yaml +26 -0
  66. massgen/configs/{three_agents_default.yaml → basic/multi/three_agents_default.yaml} +8 -4
  67. massgen/configs/basic/multi/three_agents_opensource.yaml +27 -0
  68. massgen/configs/basic/multi/three_agents_vllm.yaml +20 -0
  69. massgen/configs/basic/multi/two_agents_gemini.yaml +19 -0
  70. massgen/configs/{two_agents.yaml → basic/multi/two_agents_gpt5.yaml} +14 -6
  71. massgen/configs/basic/multi/two_agents_opensource_lmstudio.yaml +31 -0
  72. massgen/configs/basic/multi/two_qwen_vllm_sglang.yaml +28 -0
  73. massgen/configs/{single_agent.yaml → basic/single/single_agent.yaml} +1 -1
  74. massgen/configs/{single_flash2.5.yaml → basic/single/single_flash2.5.yaml} +1 -2
  75. massgen/configs/basic/single/single_gemini2.5pro.yaml +16 -0
  76. massgen/configs/basic/single/single_gpt4o_audio_generation.yaml +22 -0
  77. massgen/configs/basic/single/single_gpt4o_image_generation.yaml +22 -0
  78. massgen/configs/basic/single/single_gpt4o_video_generation.yaml +24 -0
  79. massgen/configs/basic/single/single_gpt5nano.yaml +20 -0
  80. massgen/configs/basic/single/single_gpt5nano_file_search.yaml +18 -0
  81. massgen/configs/basic/single/single_gpt5nano_image_understanding.yaml +17 -0
  82. massgen/configs/basic/single/single_gptoss120b.yaml +15 -0
  83. massgen/configs/basic/single/single_openrouter_audio_understanding.yaml +15 -0
  84. massgen/configs/basic/single/single_qwen_video_understanding.yaml +15 -0
  85. massgen/configs/debug/code_execution/command_filtering_blacklist.yaml +29 -0
  86. massgen/configs/debug/code_execution/command_filtering_whitelist.yaml +28 -0
  87. massgen/configs/debug/code_execution/docker_verification.yaml +29 -0
  88. massgen/configs/debug/skip_coordination_test.yaml +27 -0
  89. massgen/configs/debug/test_sdk_migration.yaml +17 -0
  90. massgen/configs/docs/DISCORD_MCP_SETUP.md +208 -0
  91. massgen/configs/docs/TWITTER_MCP_ENESCINAR_SETUP.md +82 -0
  92. massgen/configs/providers/azure/azure_openai_multi.yaml +21 -0
  93. massgen/configs/providers/azure/azure_openai_single.yaml +19 -0
  94. massgen/configs/providers/claude/claude.yaml +14 -0
  95. massgen/configs/providers/gemini/gemini_gpt5nano.yaml +28 -0
  96. massgen/configs/providers/local/lmstudio.yaml +11 -0
  97. massgen/configs/providers/openai/gpt5.yaml +46 -0
  98. massgen/configs/providers/openai/gpt5_nano.yaml +46 -0
  99. massgen/configs/providers/others/grok_single_agent.yaml +19 -0
  100. massgen/configs/providers/others/zai_coding_team.yaml +108 -0
  101. massgen/configs/providers/others/zai_glm45.yaml +12 -0
  102. massgen/configs/{creative_team.yaml → teams/creative/creative_team.yaml} +16 -6
  103. massgen/configs/{travel_planning.yaml → teams/creative/travel_planning.yaml} +16 -6
  104. massgen/configs/{news_analysis.yaml → teams/research/news_analysis.yaml} +16 -6
  105. massgen/configs/{research_team.yaml → teams/research/research_team.yaml} +15 -7
  106. massgen/configs/{technical_analysis.yaml → teams/research/technical_analysis.yaml} +16 -6
  107. massgen/configs/tools/code-execution/basic_command_execution.yaml +25 -0
  108. massgen/configs/tools/code-execution/code_execution_use_case_simple.yaml +41 -0
  109. massgen/configs/tools/code-execution/docker_claude_code.yaml +32 -0
  110. massgen/configs/tools/code-execution/docker_multi_agent.yaml +32 -0
  111. massgen/configs/tools/code-execution/docker_simple.yaml +29 -0
  112. massgen/configs/tools/code-execution/docker_with_resource_limits.yaml +32 -0
  113. massgen/configs/tools/code-execution/multi_agent_playwright_automation.yaml +57 -0
  114. massgen/configs/tools/filesystem/cc_gpt5_gemini_filesystem.yaml +34 -0
  115. massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +68 -0
  116. massgen/configs/tools/filesystem/claude_code_flash2.5.yaml +43 -0
  117. massgen/configs/tools/filesystem/claude_code_flash2.5_gptoss.yaml +49 -0
  118. massgen/configs/tools/filesystem/claude_code_gpt5nano.yaml +31 -0
  119. massgen/configs/tools/filesystem/claude_code_single.yaml +40 -0
  120. massgen/configs/tools/filesystem/fs_permissions_test.yaml +87 -0
  121. massgen/configs/tools/filesystem/gemini_gemini_workspace_cleanup.yaml +54 -0
  122. massgen/configs/tools/filesystem/gemini_gpt5_filesystem_casestudy.yaml +30 -0
  123. massgen/configs/tools/filesystem/gemini_gpt5nano_file_context_path.yaml +43 -0
  124. massgen/configs/tools/filesystem/gemini_gpt5nano_protected_paths.yaml +45 -0
  125. massgen/configs/tools/filesystem/gpt5mini_cc_fs_context_path.yaml +31 -0
  126. massgen/configs/tools/filesystem/grok4_gpt5_gemini_filesystem.yaml +32 -0
  127. massgen/configs/tools/filesystem/multiturn/grok4_gpt5_claude_code_filesystem_multiturn.yaml +58 -0
  128. massgen/configs/tools/filesystem/multiturn/grok4_gpt5_gemini_filesystem_multiturn.yaml +58 -0
  129. massgen/configs/tools/filesystem/multiturn/two_claude_code_filesystem_multiturn.yaml +47 -0
  130. massgen/configs/tools/filesystem/multiturn/two_gemini_flash_filesystem_multiturn.yaml +48 -0
  131. massgen/configs/tools/mcp/claude_code_discord_mcp_example.yaml +27 -0
  132. massgen/configs/tools/mcp/claude_code_simple_mcp.yaml +35 -0
  133. massgen/configs/tools/mcp/claude_code_twitter_mcp_example.yaml +32 -0
  134. massgen/configs/tools/mcp/claude_mcp_example.yaml +24 -0
  135. massgen/configs/tools/mcp/claude_mcp_test.yaml +27 -0
  136. massgen/configs/tools/mcp/five_agents_travel_mcp_test.yaml +157 -0
  137. massgen/configs/tools/mcp/five_agents_weather_mcp_test.yaml +103 -0
  138. massgen/configs/tools/mcp/gemini_mcp_example.yaml +24 -0
  139. massgen/configs/tools/mcp/gemini_mcp_filesystem_test.yaml +23 -0
  140. massgen/configs/tools/mcp/gemini_mcp_filesystem_test_sharing.yaml +23 -0
  141. massgen/configs/tools/mcp/gemini_mcp_filesystem_test_single_agent.yaml +17 -0
  142. massgen/configs/tools/mcp/gemini_mcp_filesystem_test_with_claude_code.yaml +24 -0
  143. massgen/configs/tools/mcp/gemini_mcp_test.yaml +27 -0
  144. massgen/configs/tools/mcp/gemini_notion_mcp.yaml +52 -0
  145. massgen/configs/tools/mcp/gpt5_nano_mcp_example.yaml +24 -0
  146. massgen/configs/tools/mcp/gpt5_nano_mcp_test.yaml +27 -0
  147. massgen/configs/tools/mcp/gpt5mini_claude_code_discord_mcp_example.yaml +38 -0
  148. massgen/configs/tools/mcp/gpt_oss_mcp_example.yaml +25 -0
  149. massgen/configs/tools/mcp/gpt_oss_mcp_test.yaml +28 -0
  150. massgen/configs/tools/mcp/grok3_mini_mcp_example.yaml +24 -0
  151. massgen/configs/tools/mcp/grok3_mini_mcp_test.yaml +27 -0
  152. massgen/configs/tools/mcp/multimcp_gemini.yaml +111 -0
  153. massgen/configs/tools/mcp/qwen_api_mcp_example.yaml +25 -0
  154. massgen/configs/tools/mcp/qwen_api_mcp_test.yaml +28 -0
  155. massgen/configs/tools/mcp/qwen_local_mcp_example.yaml +24 -0
  156. massgen/configs/tools/mcp/qwen_local_mcp_test.yaml +27 -0
  157. massgen/configs/tools/planning/five_agents_discord_mcp_planning_mode.yaml +140 -0
  158. massgen/configs/tools/planning/five_agents_filesystem_mcp_planning_mode.yaml +151 -0
  159. massgen/configs/tools/planning/five_agents_notion_mcp_planning_mode.yaml +151 -0
  160. massgen/configs/tools/planning/five_agents_twitter_mcp_planning_mode.yaml +155 -0
  161. massgen/configs/tools/planning/gpt5_mini_case_study_mcp_planning_mode.yaml +73 -0
  162. massgen/configs/tools/web-search/claude_streamable_http_test.yaml +43 -0
  163. massgen/configs/tools/web-search/gemini_streamable_http_test.yaml +43 -0
  164. massgen/configs/tools/web-search/gpt5_mini_streamable_http_test.yaml +43 -0
  165. massgen/configs/tools/web-search/gpt_oss_streamable_http_test.yaml +44 -0
  166. massgen/configs/tools/web-search/grok3_mini_streamable_http_test.yaml +43 -0
  167. massgen/configs/tools/web-search/qwen_api_streamable_http_test.yaml +44 -0
  168. massgen/configs/tools/web-search/qwen_local_streamable_http_test.yaml +43 -0
  169. massgen/coordination_tracker.py +708 -0
  170. massgen/docker/README.md +462 -0
  171. massgen/filesystem_manager/__init__.py +21 -0
  172. massgen/filesystem_manager/_base.py +9 -0
  173. massgen/filesystem_manager/_code_execution_server.py +545 -0
  174. massgen/filesystem_manager/_docker_manager.py +477 -0
  175. massgen/filesystem_manager/_file_operation_tracker.py +248 -0
  176. massgen/filesystem_manager/_filesystem_manager.py +813 -0
  177. massgen/filesystem_manager/_path_permission_manager.py +1261 -0
  178. massgen/filesystem_manager/_workspace_tools_server.py +1815 -0
  179. massgen/formatter/__init__.py +10 -0
  180. massgen/formatter/_chat_completions_formatter.py +284 -0
  181. massgen/formatter/_claude_formatter.py +235 -0
  182. massgen/formatter/_formatter_base.py +156 -0
  183. massgen/formatter/_response_formatter.py +263 -0
  184. massgen/frontend/__init__.py +1 -2
  185. massgen/frontend/coordination_ui.py +471 -286
  186. massgen/frontend/displays/base_display.py +56 -11
  187. massgen/frontend/displays/create_coordination_table.py +1956 -0
  188. massgen/frontend/displays/rich_terminal_display.py +1259 -619
  189. massgen/frontend/displays/simple_display.py +9 -4
  190. massgen/frontend/displays/terminal_display.py +27 -68
  191. massgen/logger_config.py +681 -0
  192. massgen/mcp_tools/README.md +232 -0
  193. massgen/mcp_tools/__init__.py +105 -0
  194. massgen/mcp_tools/backend_utils.py +1035 -0
  195. massgen/mcp_tools/circuit_breaker.py +195 -0
  196. massgen/mcp_tools/client.py +894 -0
  197. massgen/mcp_tools/config_validator.py +138 -0
  198. massgen/mcp_tools/docs/circuit_breaker.md +646 -0
  199. massgen/mcp_tools/docs/client.md +950 -0
  200. massgen/mcp_tools/docs/config_validator.md +478 -0
  201. massgen/mcp_tools/docs/exceptions.md +1165 -0
  202. massgen/mcp_tools/docs/security.md +854 -0
  203. massgen/mcp_tools/exceptions.py +338 -0
  204. massgen/mcp_tools/hooks.py +212 -0
  205. massgen/mcp_tools/security.py +780 -0
  206. massgen/message_templates.py +342 -64
  207. massgen/orchestrator.py +1515 -241
  208. massgen/stream_chunk/__init__.py +35 -0
  209. massgen/stream_chunk/base.py +92 -0
  210. massgen/stream_chunk/multimodal.py +237 -0
  211. massgen/stream_chunk/text.py +162 -0
  212. massgen/tests/mcp_test_server.py +150 -0
  213. massgen/tests/multi_turn_conversation_design.md +0 -8
  214. massgen/tests/test_azure_openai_backend.py +156 -0
  215. massgen/tests/test_backend_capabilities.py +262 -0
  216. massgen/tests/test_backend_event_loop_all.py +179 -0
  217. massgen/tests/test_chat_completions_refactor.py +142 -0
  218. massgen/tests/test_claude_backend.py +15 -28
  219. massgen/tests/test_claude_code.py +268 -0
  220. massgen/tests/test_claude_code_context_sharing.py +233 -0
  221. massgen/tests/test_claude_code_orchestrator.py +175 -0
  222. massgen/tests/test_cli_backends.py +180 -0
  223. massgen/tests/test_code_execution.py +679 -0
  224. massgen/tests/test_external_agent_backend.py +134 -0
  225. massgen/tests/test_final_presentation_fallback.py +237 -0
  226. massgen/tests/test_gemini_planning_mode.py +351 -0
  227. massgen/tests/test_grok_backend.py +7 -10
  228. massgen/tests/test_http_mcp_server.py +42 -0
  229. massgen/tests/test_integration_simple.py +198 -0
  230. massgen/tests/test_mcp_blocking.py +125 -0
  231. massgen/tests/test_message_context_building.py +29 -47
  232. massgen/tests/test_orchestrator_final_presentation.py +48 -0
  233. massgen/tests/test_path_permission_manager.py +2087 -0
  234. massgen/tests/test_rich_terminal_display.py +14 -13
  235. massgen/tests/test_timeout.py +133 -0
  236. massgen/tests/test_v3_3agents.py +11 -12
  237. massgen/tests/test_v3_simple.py +8 -13
  238. massgen/tests/test_v3_three_agents.py +11 -18
  239. massgen/tests/test_v3_two_agents.py +8 -13
  240. massgen/token_manager/__init__.py +7 -0
  241. massgen/token_manager/token_manager.py +400 -0
  242. massgen/utils.py +52 -16
  243. massgen/v1/agent.py +45 -91
  244. massgen/v1/agents.py +18 -53
  245. massgen/v1/backends/gemini.py +50 -153
  246. massgen/v1/backends/grok.py +21 -54
  247. massgen/v1/backends/oai.py +39 -111
  248. massgen/v1/cli.py +36 -93
  249. massgen/v1/config.py +8 -12
  250. massgen/v1/logging.py +43 -127
  251. massgen/v1/main.py +18 -32
  252. massgen/v1/orchestrator.py +68 -209
  253. massgen/v1/streaming_display.py +62 -163
  254. massgen/v1/tools.py +8 -12
  255. massgen/v1/types.py +9 -23
  256. massgen/v1/utils.py +5 -23
  257. massgen-0.1.0.dist-info/METADATA +1245 -0
  258. massgen-0.1.0.dist-info/RECORD +273 -0
  259. massgen-0.1.0.dist-info/entry_points.txt +2 -0
  260. massgen/frontend/logging/__init__.py +0 -9
  261. massgen/frontend/logging/realtime_logger.py +0 -197
  262. massgen-0.0.3.dist-info/METADATA +0 -568
  263. massgen-0.0.3.dist-info/RECORD +0 -76
  264. massgen-0.0.3.dist-info/entry_points.txt +0 -2
  265. /massgen/backend/{Function calling openai responses.md → docs/Function calling openai responses.md} +0 -0
  266. {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/WHEEL +0 -0
  267. {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/licenses/LICENSE +0 -0
  268. {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,813 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Filesystem Manager for MassGen - Handles workspace and snapshot management.
4
+
5
+ This manager provides centralized filesystem operations for backends that support
6
+ filesystem access through MCP. It manages:
7
+ - Workspace directory creation and cleanup
8
+ - Permission management for various path types
9
+ - Snapshot storage for context sharing
10
+ - Temporary workspace restoration
11
+ - Additional context paths
12
+ - Path configuration for MCP filesystem server
13
+
14
+ The manager is backend-agnostic and works with any backend that has filesystem
15
+ MCP tools configured.
16
+ """
17
+
18
+ import os
19
+ import shutil
20
+ from datetime import datetime
21
+ from pathlib import Path
22
+ from typing import Any, Dict, List, Optional
23
+
24
+ from ..logger_config import get_log_session_dir, logger
25
+ from ..mcp_tools.client import HookType
26
+ from . import _code_execution_server as ce_module
27
+ from . import _workspace_tools_server as wc_module
28
+ from ._base import Permission
29
+ from ._path_permission_manager import PathPermissionManager
30
+
31
+
32
+ class FilesystemManager:
33
+ """
34
+ Manages filesystem operations for backends with MCP filesystem support.
35
+
36
+ This class handles:
37
+ - Workspace directory lifecycle (creation, cleanup)
38
+ - Snapshot storage and restoration for context sharing
39
+ - Path management for MCP filesystem server configuration
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ cwd: str,
45
+ agent_temporary_workspace_parent: str = None,
46
+ context_paths: List[Dict[str, Any]] = None,
47
+ context_write_access_enabled: bool = False,
48
+ enforce_read_before_delete: bool = True,
49
+ enable_image_generation: bool = False,
50
+ enable_mcp_command_line: bool = False,
51
+ command_line_allowed_commands: List[str] = None,
52
+ command_line_blocked_commands: List[str] = None,
53
+ command_line_execution_mode: str = "local",
54
+ command_line_docker_image: str = "massgen/mcp-runtime:latest",
55
+ command_line_docker_memory_limit: Optional[str] = None,
56
+ command_line_docker_cpu_limit: Optional[float] = None,
57
+ command_line_docker_network_mode: str = "none",
58
+ enable_audio_generation: bool = False,
59
+ ):
60
+ """
61
+ Initialize FilesystemManager.
62
+
63
+ Args:
64
+ cwd: Working directory path for the agent
65
+ agent_temporary_workspace_parent: Parent directory for temporary workspaces
66
+ context_paths: List of context path configurations for access control
67
+ context_write_access_enabled: Whether write access is enabled for context paths
68
+ enforce_read_before_delete: Whether to enforce read-before-delete policy for workspace files
69
+ enable_image_generation: Whether to enable image generation tools
70
+ enable_mcp_command_line: Whether to enable MCP command line execution tool
71
+ command_line_allowed_commands: Whitelist of allowed command patterns (regex)
72
+ command_line_blocked_commands: Blacklist of blocked command patterns (regex)
73
+ command_line_execution_mode: Execution mode - "local" or "docker"
74
+ command_line_docker_image: Docker image to use for containers
75
+ command_line_docker_memory_limit: Memory limit for Docker containers (e.g., "2g")
76
+ command_line_docker_cpu_limit: CPU limit for Docker containers (e.g., 2.0 for 2 CPUs)
77
+ command_line_docker_network_mode: Network mode for Docker containers (none/bridge/host)
78
+ """
79
+ self.agent_id = None # Will be set by orchestrator via setup_orchestration_paths
80
+ self.enable_image_generation = enable_image_generation
81
+ self.enable_mcp_command_line = enable_mcp_command_line
82
+ self.command_line_allowed_commands = command_line_allowed_commands
83
+ self.command_line_blocked_commands = command_line_blocked_commands
84
+ self.command_line_execution_mode = command_line_execution_mode
85
+ self.command_line_docker_image = command_line_docker_image
86
+ self.command_line_docker_memory_limit = command_line_docker_memory_limit
87
+ self.command_line_docker_cpu_limit = command_line_docker_cpu_limit
88
+ self.command_line_docker_network_mode = command_line_docker_network_mode
89
+
90
+ # Initialize Docker manager if Docker mode enabled
91
+ self.docker_manager = None
92
+ if enable_mcp_command_line and command_line_execution_mode == "docker":
93
+ from ._docker_manager import DockerManager
94
+
95
+ self.docker_manager = DockerManager(
96
+ image=command_line_docker_image,
97
+ network_mode=command_line_docker_network_mode,
98
+ memory_limit=command_line_docker_memory_limit,
99
+ cpu_limit=command_line_docker_cpu_limit,
100
+ )
101
+ self.enable_audio_generation = enable_audio_generation
102
+
103
+ # Initialize path permission manager
104
+ self.path_permission_manager = PathPermissionManager(
105
+ context_write_access_enabled=context_write_access_enabled,
106
+ enforce_read_before_delete=enforce_read_before_delete,
107
+ )
108
+
109
+ # Add context paths if provided
110
+ if context_paths:
111
+ self.path_permission_manager.add_context_paths(context_paths)
112
+
113
+ # Set agent_temporary_workspace_parent first, before calling _setup_workspace
114
+ self.agent_temporary_workspace_parent = agent_temporary_workspace_parent
115
+
116
+ # Get absolute path for temporary workspace parent if provided
117
+ if self.agent_temporary_workspace_parent:
118
+ # Add parent directory prefix for temp workspaces if not already present
119
+ temp_parent = self.agent_temporary_workspace_parent
120
+
121
+ temp_parent_path = Path(temp_parent)
122
+ if not temp_parent_path.is_absolute():
123
+ temp_parent_path = temp_parent_path.resolve()
124
+ self.agent_temporary_workspace_parent = temp_parent_path
125
+ # Clear existing temp workspace parent if it exists, else we would only clear those with the exact agent_ids in the config.
126
+ self.clear_temp_workspace()
127
+
128
+ # Setup main working directory (now that agent_temporary_workspace_parent is set)
129
+ self.cwd = self._setup_workspace(cwd)
130
+
131
+ # Add workspace to path manager (workspace is typically writable)
132
+ self.path_permission_manager.add_path(self.cwd, Permission.WRITE, "workspace")
133
+ # Add temporary workspace to path manager (read-only)
134
+ self.path_permission_manager.add_path(self.agent_temporary_workspace_parent, Permission.READ, "temp_workspace")
135
+
136
+ # Orchestration-specific paths (set by setup_orchestration_paths)
137
+ self.snapshot_storage = None # Path for storing workspace snapshots
138
+ self.agent_temporary_workspace = None # Full path for this specific agent's temporary workspace
139
+
140
+ # Track whether we're using a temporary workspace
141
+ self._using_temporary = False
142
+ self._original_cwd = self.cwd
143
+
144
+ def setup_orchestration_paths(
145
+ self,
146
+ agent_id: str,
147
+ snapshot_storage: Optional[str] = None,
148
+ agent_temporary_workspace: Optional[str] = None,
149
+ ) -> None:
150
+ """
151
+ Setup orchestration-specific paths for snapshots and temporary workspace.
152
+ Called by orchestrator to configure paths for this specific orchestration.
153
+
154
+ Args:
155
+ agent_id: The agent identifier for this orchestration
156
+ snapshot_storage: Base path for storing workspace snapshots
157
+ agent_temporary_workspace: Base path for temporary workspace during context sharing
158
+ """
159
+ logger.info(f"[FilesystemManager.setup_orchestration_paths] Called for agent_id={agent_id}, snapshot_storage={snapshot_storage}, agent_temporary_workspace={agent_temporary_workspace}")
160
+ self.agent_id = agent_id
161
+
162
+ # Setup snapshot storage if provided
163
+ if snapshot_storage and self.agent_id:
164
+ self.snapshot_storage = Path(snapshot_storage) / self.agent_id
165
+ self.snapshot_storage.mkdir(parents=True, exist_ok=True)
166
+
167
+ # Setup temporary workspace for context sharing
168
+ if agent_temporary_workspace and self.agent_id:
169
+ self.agent_temporary_workspace = self._setup_workspace(self.agent_temporary_workspace_parent / self.agent_id)
170
+
171
+ # Also setup log directories if we have an agent_id
172
+ if self.agent_id:
173
+ log_session_dir = get_log_session_dir()
174
+ if log_session_dir:
175
+ agent_log_dir = log_session_dir / self.agent_id
176
+ agent_log_dir.mkdir(parents=True, exist_ok=True)
177
+
178
+ # Create Docker container if Docker mode enabled
179
+ if self.docker_manager and self.agent_id:
180
+ context_paths = self.path_permission_manager.get_context_paths()
181
+ self.docker_manager.create_container(
182
+ agent_id=self.agent_id,
183
+ workspace_path=self.cwd,
184
+ temp_workspace_path=self.agent_temporary_workspace_parent if self.agent_temporary_workspace_parent else None,
185
+ context_paths=context_paths,
186
+ )
187
+ logger.info(f"[FilesystemManager] Docker container created for agent {self.agent_id}")
188
+
189
+ def update_backend_mcp_config(self, backend_config: Dict[str, Any]) -> Dict[str, Any]:
190
+ """
191
+ Update MCP server configuration with agent_id after it's available.
192
+
193
+ This should be called by the backend after setup_orchestration_paths() sets agent_id.
194
+
195
+ Args:
196
+ backend_config: Backend configuration dict containing mcp_servers
197
+
198
+ Returns:
199
+ Updated backend configuration
200
+ """
201
+ if not self.enable_mcp_command_line or self.command_line_execution_mode != "docker":
202
+ return backend_config
203
+
204
+ if not self.agent_id:
205
+ logger.warning("[FilesystemManager] agent_id not set, cannot update MCP config for Docker mode")
206
+ return backend_config
207
+
208
+ # Update command_line MCP server config to include --agent-id
209
+ mcp_servers = backend_config.get("mcp_servers", [])
210
+ for server in mcp_servers:
211
+ if isinstance(server, dict) and server.get("name") == "command_line":
212
+ args = server.get("args", [])
213
+ # Check if --agent-id is already in args
214
+ if "--agent-id" not in args:
215
+ args.extend(["--agent-id", self.agent_id])
216
+ server["args"] = args
217
+ logger.info(f"[FilesystemManager] Updated command_line MCP server config with agent_id: {self.agent_id}")
218
+ break
219
+
220
+ return backend_config
221
+
222
+ def _setup_workspace(self, cwd: str) -> Path:
223
+ """Setup workspace directory, creating if needed and clearing existing files safely."""
224
+ # Add parent directory prefix if not already present
225
+ Path(cwd)
226
+ workspace = Path(cwd).resolve()
227
+
228
+ # Safety checks
229
+ if not workspace.is_absolute():
230
+ raise AssertionError("Workspace must be absolute")
231
+ if workspace == Path("/") or len(workspace.parts) < 3:
232
+ raise AssertionError(f"Refusing unsafe workspace path: {workspace}")
233
+
234
+ # Create if needed
235
+ workspace.mkdir(parents=True, exist_ok=True)
236
+
237
+ # Clear existing contents
238
+ if workspace.exists() and workspace.is_dir():
239
+ for item in workspace.iterdir():
240
+ if item.is_symlink():
241
+ logger.warning(f"[FilesystemManager.save_snapshot] Skipping symlink during clear: {item}")
242
+ if item.is_file():
243
+ item.unlink()
244
+ elif item.is_dir():
245
+ shutil.rmtree(item)
246
+
247
+ return workspace
248
+
249
+ def get_mcp_filesystem_config(self) -> Dict[str, Any]:
250
+ """
251
+ Generate MCP filesystem server configuration.
252
+
253
+ Returns:
254
+ Dictionary with MCP server configuration for filesystem access
255
+ """
256
+ # Get all managed paths
257
+ paths = self.path_permission_manager.get_mcp_filesystem_paths()
258
+
259
+ # Build MCP server configuration with all managed paths
260
+ config = {
261
+ "name": "filesystem",
262
+ "type": "stdio",
263
+ "command": "npx",
264
+ "args": [
265
+ "-y",
266
+ "@modelcontextprotocol/server-filesystem",
267
+ ]
268
+ + paths,
269
+ "cwd": str(self.cwd), # Set working directory for filesystem server (important for relative paths)
270
+ # Exclude read_media_file since we have our own implementation in workspace_tools
271
+ # Note: Tool names here are unprefixed (before server name is added)
272
+ "exclude_tools": ["read_media_file"],
273
+ }
274
+
275
+ return config
276
+
277
+ def get_workspace_tools_mcp_config(self) -> Dict[str, Any]:
278
+ """
279
+ Generate workspace tools MCP server configuration.
280
+
281
+ Returns:
282
+ Dictionary with MCP server configuration for workspace tools (copy, delete, compare)
283
+ """
284
+ # Get context paths using the existing method
285
+ context_paths = self.path_permission_manager.get_context_paths()
286
+ ",".join([cp["path"] for cp in context_paths])
287
+
288
+ # Get absolute path to the workspace tools server script
289
+ script_path = Path(wc_module.__file__).resolve()
290
+
291
+ # Pass allowed paths
292
+ paths = self.path_permission_manager.get_mcp_filesystem_paths()
293
+
294
+ env = {
295
+ "FASTMCP_SHOW_CLI_BANNER": "false",
296
+ }
297
+
298
+ config = {
299
+ "name": "workspace_tools",
300
+ "type": "stdio",
301
+ "command": "fastmcp",
302
+ "args": ["run", f"{script_path}:create_server"] + ["--", "--allowed-paths"] + paths,
303
+ "env": env,
304
+ "cwd": str(self.cwd),
305
+ }
306
+
307
+ # Conditionally exclude image generation tools if not enabled
308
+ if not self.enable_image_generation:
309
+ config["exclude_tools"] = [
310
+ "generate_and_store_image_with_input_images",
311
+ "generate_and_store_image_no_input_images",
312
+ ]
313
+ if not self.enable_audio_generation:
314
+ if "exclude_tools" not in config:
315
+ config["exclude_tools"] = []
316
+ config["exclude_tools"].extend(
317
+ [
318
+ "generate_and_store_audio_with_input_audios",
319
+ "generate_and_store_audio_no_input_audios",
320
+ ],
321
+ )
322
+
323
+ return config
324
+
325
+ def get_command_line_mcp_config(self) -> Dict[str, Any]:
326
+ """
327
+ Generate command line execution MCP server configuration.
328
+
329
+ Returns:
330
+ Dictionary with MCP server configuration for command execution
331
+ (supports bash on Unix/Mac, cmd/PowerShell on Windows, and Docker isolation)
332
+ """
333
+ # Get absolute path to the code execution server script
334
+ script_path = Path(ce_module.__file__).resolve()
335
+
336
+ # Pass allowed paths
337
+ paths = self.path_permission_manager.get_mcp_filesystem_paths()
338
+
339
+ env = {
340
+ "FASTMCP_SHOW_CLI_BANNER": "false",
341
+ }
342
+
343
+ # Pass DOCKER_HOST environment variable if present
344
+ if "DOCKER_HOST" in os.environ:
345
+ env["DOCKER_HOST"] = os.environ["DOCKER_HOST"]
346
+
347
+ config = {
348
+ "name": "command_line",
349
+ "type": "stdio",
350
+ "command": "fastmcp",
351
+ "args": ["run", f"{script_path}:create_server", "--", "--allowed-paths"] + paths,
352
+ "env": env,
353
+ "cwd": str(self.cwd),
354
+ }
355
+
356
+ # Add execution mode
357
+ config["args"].extend(["--execution-mode", self.command_line_execution_mode])
358
+
359
+ # Add agent ID for Docker mode
360
+ if self.command_line_execution_mode == "docker" and self.agent_id:
361
+ config["args"].extend(["--agent-id", self.agent_id])
362
+
363
+ # Add command filters if specified
364
+ if self.command_line_allowed_commands:
365
+ config["args"].extend(["--allowed-commands"] + self.command_line_allowed_commands)
366
+
367
+ if self.command_line_blocked_commands:
368
+ config["args"].extend(["--blocked-commands"] + self.command_line_blocked_commands)
369
+
370
+ return config
371
+
372
+ def inject_filesystem_mcp(self, backend_config: Dict[str, Any]) -> Dict[str, Any]:
373
+ """
374
+ Inject filesystem and workspace tools MCP servers into backend configuration.
375
+
376
+ Args:
377
+ backend_config: Original backend configuration
378
+
379
+ Returns:
380
+ Modified configuration with MCP servers added
381
+ """
382
+ # Get existing mcp_servers configuration
383
+ mcp_servers = backend_config.get("mcp_servers", [])
384
+
385
+ # Handle both list format and Claude Code dict format
386
+ if isinstance(mcp_servers, dict):
387
+ # Claude Code format: {"playwright": {...}, "filesystem": {...}}
388
+ existing_names = list(mcp_servers.keys())
389
+ # Convert to list format for append operations
390
+ converted_servers = []
391
+ for name, server_config in mcp_servers.items():
392
+ if isinstance(server_config, dict):
393
+ server = server_config.copy()
394
+ server["name"] = name
395
+ converted_servers.append(server)
396
+ mcp_servers = converted_servers
397
+ elif isinstance(mcp_servers, list):
398
+ # List format: [{"name": "playwright", ...}, ...]
399
+ existing_names = [server.get("name") for server in mcp_servers if isinstance(server, dict)]
400
+ else:
401
+ existing_names = []
402
+ mcp_servers = []
403
+
404
+ try:
405
+ # Add filesystem server if missing
406
+ if "filesystem" not in existing_names:
407
+ mcp_servers.append(self.get_mcp_filesystem_config())
408
+ else:
409
+ logger.warning("[FilesystemManager.inject_filesystem_mcp] Custom filesystem MCP server already present")
410
+
411
+ # Add workspace tools server if missing
412
+ if "workspace_tools" not in existing_names:
413
+ mcp_servers.append(self.get_workspace_tools_mcp_config())
414
+ else:
415
+ logger.warning("[FilesystemManager.inject_filesystem_mcp] Custom workspace_tools MCP server already present")
416
+
417
+ # Add command line server if enabled and missing
418
+ if self.enable_mcp_command_line and "command_line" not in existing_names:
419
+ mcp_servers.append(self.get_command_line_mcp_config())
420
+ elif self.enable_mcp_command_line:
421
+ logger.warning("[FilesystemManager.inject_filesystem_mcp] Custom command_line MCP server already present")
422
+
423
+ except Exception as e:
424
+ logger.warning(f"[FilesystemManager.inject_filesystem_mcp] Error checking existing MCP servers: {e}")
425
+
426
+ # Update backend config
427
+ backend_config["mcp_servers"] = mcp_servers
428
+
429
+ return backend_config
430
+
431
+ def inject_command_line_mcp(self, backend_config: Dict[str, Any]) -> Dict[str, Any]:
432
+ """
433
+ Inject only the command_line MCP server into backend configuration.
434
+
435
+ Used for NATIVE backends (like Claude Code) that have built-in filesystem tools
436
+ but need the execute_command MCP tool when using docker mode for code execution.
437
+
438
+ Args:
439
+ backend_config: Original backend configuration
440
+
441
+ Returns:
442
+ Modified configuration with command_line MCP server added
443
+ """
444
+ # Get existing mcp_servers configuration
445
+ mcp_servers = backend_config.get("mcp_servers", [])
446
+
447
+ # Handle both list format and Claude Code dict format
448
+ if isinstance(mcp_servers, dict):
449
+ # Claude Code format: {"playwright": {...}, "command_line": {...}}
450
+ existing_names = list(mcp_servers.keys())
451
+ # Convert to list format for append operations
452
+ converted_servers = []
453
+ for name, server_config in mcp_servers.items():
454
+ if isinstance(server_config, dict):
455
+ server = server_config.copy()
456
+ server["name"] = name
457
+ converted_servers.append(server)
458
+ mcp_servers = converted_servers
459
+ elif isinstance(mcp_servers, list):
460
+ # List format: [{"name": "playwright", ...}, ...]
461
+ existing_names = [server.get("name") for server in mcp_servers if isinstance(server, dict)]
462
+ else:
463
+ existing_names = []
464
+ mcp_servers = []
465
+
466
+ try:
467
+ # Add command line server if missing (only called for docker mode)
468
+ if "command_line" not in existing_names:
469
+ mcp_servers.append(self.get_command_line_mcp_config())
470
+ logger.info("[FilesystemManager.inject_command_line_mcp] Added command_line MCP server for docker mode")
471
+ else:
472
+ logger.warning("[FilesystemManager.inject_command_line_mcp] Custom command_line MCP server already present")
473
+
474
+ except Exception as e:
475
+ logger.warning(f"[FilesystemManager.inject_command_line_mcp] Error adding command_line MCP server: {e}")
476
+
477
+ # Update backend config
478
+ backend_config["mcp_servers"] = mcp_servers
479
+
480
+ return backend_config
481
+
482
+ def get_pre_tool_hooks(self) -> Dict[str, List]:
483
+ """
484
+ Get pre-tool hooks configuration for MCP clients.
485
+
486
+ Returns:
487
+ Dict mapping hook types to lists of hook functions
488
+ """
489
+
490
+ async def mcp_hook_wrapper(tool_name: str, tool_args: Dict[str, Any]) -> bool:
491
+ """Wrapper to adapt our hook signature to MCP client expectations."""
492
+ allowed, reason = await self.path_permission_manager.pre_tool_use_hook(tool_name, tool_args)
493
+ if not allowed and reason:
494
+ logger.warning(f"[FilesystemManager] Tool blocked: {tool_name} - {reason}")
495
+ return allowed
496
+
497
+ return {HookType.PRE_TOOL_USE: [mcp_hook_wrapper]}
498
+
499
+ def get_claude_code_hooks_config(self) -> Dict[str, Any]:
500
+ """
501
+ Get Claude Agent SDK hooks configuration.
502
+
503
+ Returns:
504
+ Hooks configuration dict for ClaudeAgentOptions
505
+ """
506
+ return self.path_permission_manager.get_claude_code_hooks_config()
507
+
508
+ def enable_write_access(self) -> None:
509
+ """
510
+ Enable write access for this filesystem manager.
511
+
512
+ This should be called for final agents to allow them to modify
513
+ files with write permissions in their context paths.
514
+ """
515
+ self.path_permission_manager.context_write_access_enabled = True
516
+ logger.info("[FilesystemManager] Context write access enabled - agent can now modify files with write permissions")
517
+
518
+ async def save_snapshot(self, timestamp: Optional[str] = None, is_final: bool = False) -> None:
519
+ """
520
+ Save a snapshot of the workspace. Always saves to snapshot_storage if available (keeping only most recent).
521
+ Additionally saves to log directories if logging is enabled.
522
+ Then, clear the workspace so it is ready for next execution.
523
+
524
+ Args:
525
+ timestamp: Optional timestamp to use for the snapshot directory (if not provided, generates one)
526
+ is_final: If True, save as final snapshot for presentation
527
+
528
+ TODO: reimplement without 'shutil' and 'os' operations for true async, though we may not need to worry about race conditions here since only one agent writes at a time
529
+ """
530
+ logger.info(f"[FilesystemManager.save_snapshot] Called for agent_id={self.agent_id}, is_final={is_final}, snapshot_storage={self.snapshot_storage}")
531
+
532
+ # Use current workspace as source
533
+ source_dir = self.cwd
534
+ source_path = Path(source_dir)
535
+
536
+ if not source_path.exists() or not source_path.is_dir():
537
+ logger.warning(f"[FilesystemManager] Source path invalid - exists: {source_path.exists()}, " f"is_dir: {source_path.is_dir() if source_path.exists() else False}")
538
+ return
539
+
540
+ if not any(source_path.iterdir()):
541
+ logger.warning(f"[FilesystemManager.save_snapshot] Source path {source_path} is empty, skipping snapshot")
542
+ return
543
+
544
+ try:
545
+ # --- 1. Save to snapshot_storage ---
546
+ if self.snapshot_storage:
547
+ if self.snapshot_storage.exists():
548
+ shutil.rmtree(self.snapshot_storage)
549
+ self.snapshot_storage.mkdir(parents=True, exist_ok=True)
550
+
551
+ items_copied = 0
552
+ for item in source_path.iterdir():
553
+ if item.is_symlink():
554
+ logger.warning(f"[FilesystemManager.save_snapshot] Skipping symlink: {item}")
555
+ continue
556
+ if item.is_file():
557
+ shutil.copy2(item, self.snapshot_storage / item.name)
558
+ elif item.is_dir():
559
+ shutil.copytree(item, self.snapshot_storage / item.name)
560
+ items_copied += 1
561
+
562
+ logger.info(f"[FilesystemManager] Saved snapshot with {items_copied} items to {self.snapshot_storage}")
563
+
564
+ # --- 2. Save to log directories ---
565
+ log_session_dir = get_log_session_dir()
566
+ if log_session_dir and self.agent_id:
567
+ if is_final:
568
+ dest_dir = log_session_dir / "final" / self.agent_id / "workspace"
569
+ if dest_dir.exists():
570
+ shutil.rmtree(dest_dir)
571
+ dest_dir.mkdir(parents=True, exist_ok=True)
572
+ logger.info(f"[FilesystemManager.save_snapshot] Final log snapshot dest_dir: {dest_dir}")
573
+ else:
574
+ if not timestamp:
575
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
576
+ dest_dir = log_session_dir / self.agent_id / timestamp / "workspace"
577
+ dest_dir.mkdir(parents=True, exist_ok=True)
578
+ logger.info(f"[FilesystemManager.save_snapshot] Regular log snapshot dest_dir: {dest_dir}")
579
+
580
+ items_copied = 0
581
+ for item in source_path.iterdir():
582
+ if item.is_symlink():
583
+ logger.warning(f"[FilesystemManager.save_snapshot] Skipping symlink: {item}")
584
+ continue
585
+ if item.is_file():
586
+ shutil.copy2(item, dest_dir / item.name)
587
+ elif item.is_dir():
588
+ shutil.copytree(item, dest_dir / item.name, dirs_exist_ok=True)
589
+ items_copied += 1
590
+
591
+ logger.info(f"[FilesystemManager] Saved {'final' if is_final else 'regular'} " f"log snapshot with {items_copied} items to {dest_dir}")
592
+
593
+ except Exception as e:
594
+ logger.exception(f"[FilesystemManager.save_snapshot] Snapshot failed: {e}")
595
+ return
596
+
597
+ logger.info("[FilesystemManager] Snapshot saved successfully, workspace preserved for logs and debugging")
598
+
599
+ def clear_workspace(self) -> None:
600
+ """
601
+ Clear the current workspace to prepare for a new agent execution.
602
+
603
+ This should be called at the START of agent execution, not at the end,
604
+ to preserve workspace contents for logging and debugging.
605
+ """
606
+ workspace_path = self.get_current_workspace()
607
+
608
+ if not workspace_path.exists() or not workspace_path.is_dir():
609
+ logger.debug(f"[FilesystemManager] Workspace does not exist or is not a directory: {workspace_path}")
610
+ return
611
+
612
+ # Safety checks
613
+ if workspace_path == Path("/") or len(workspace_path.parts) < 3:
614
+ logger.error(f"[FilesystemManager] Refusing to clear unsafe workspace path: {workspace_path}")
615
+ return
616
+
617
+ try:
618
+ logger.info("[FilesystemManager] Clearing workspace at agent startup. Current contents:")
619
+ items_to_clear = list(workspace_path.iterdir())
620
+
621
+ for item in items_to_clear:
622
+ logger.info(f" - {item}")
623
+ if item.is_symlink():
624
+ logger.warning(f"[FilesystemManager] Skipping symlink during clear: {item}")
625
+ continue
626
+ if item.is_file():
627
+ item.unlink()
628
+ elif item.is_dir():
629
+ shutil.rmtree(item)
630
+
631
+ logger.info("[FilesystemManager] Workspace cleared successfully, ready for new agent execution")
632
+
633
+ except Exception as e:
634
+ logger.error(f"[FilesystemManager] Failed to clear workspace: {e}")
635
+ # Don't raise - agent can still work with non-empty workspace
636
+
637
+ def clear_temp_workspace(self) -> None:
638
+ """
639
+ Clear the temporary workspace parent directory at orchestration startup.
640
+
641
+ This clears the entire temp workspace parent (e.g., temp_workspaces/),
642
+ removing all agent directories from previous runs to prevent cross-contamination.
643
+ """
644
+ if not self.agent_temporary_workspace_parent:
645
+ logger.debug("[FilesystemManager] No temp workspace parent configured to clear")
646
+ return
647
+
648
+ if not self.agent_temporary_workspace_parent.exists():
649
+ logger.debug(f"[FilesystemManager] Temp workspace parent does not exist: {self.agent_temporary_workspace_parent}")
650
+ return
651
+
652
+ # Safety checks
653
+ if self.agent_temporary_workspace_parent == Path("/") or len(self.agent_temporary_workspace_parent.parts) < 3:
654
+ logger.error(f"[FilesystemManager] Refusing to clear unsafe temp workspace parent path: {self.agent_temporary_workspace_parent}")
655
+ return
656
+
657
+ try:
658
+ logger.info(f"[FilesystemManager] Clearing temp workspace parent at orchestration startup: {self.agent_temporary_workspace_parent}")
659
+
660
+ items_to_clear = list(self.agent_temporary_workspace_parent.iterdir())
661
+ for item in items_to_clear:
662
+ logger.info(f" - Removing temp workspace item: {item}")
663
+ if item.is_symlink():
664
+ logger.warning(f"[FilesystemManager] Skipping symlink during temp clear: {item}")
665
+ continue
666
+ if item.is_file():
667
+ item.unlink()
668
+ elif item.is_dir():
669
+ shutil.rmtree(item)
670
+
671
+ logger.info("[FilesystemManager] Temp workspace parent cleared successfully")
672
+
673
+ except Exception as e:
674
+ logger.error(f"[FilesystemManager] Failed to clear temp workspace parent: {e}")
675
+ # Don't raise - orchestration can continue without clean temp workspace
676
+
677
+ async def copy_snapshots_to_temp_workspace(self, all_snapshots: Dict[str, Path], agent_mapping: Dict[str, str]) -> Optional[Path]:
678
+ """
679
+ Copy snapshots from multiple agents to temporary workspace for context sharing.
680
+
681
+ This method is called by the orchestrator before starting an agent that needs context from others.
682
+ It copies the latest snapshots from log directories to a temporary workspace.
683
+
684
+ Args:
685
+ all_snapshots: Dictionary mapping agent_id to snapshot path (from log directories)
686
+ agent_mapping: Dictionary mapping real agent_id to anonymous agent_id
687
+
688
+ Returns:
689
+ Path to the temporary workspace with restored snapshots
690
+
691
+ TODO: reimplement without 'shutil' and 'os' operations for true async
692
+ """
693
+ if not self.agent_temporary_workspace:
694
+ return None
695
+
696
+ # Clear existing temporary workspace
697
+ if self.agent_temporary_workspace.exists():
698
+ shutil.rmtree(self.agent_temporary_workspace)
699
+ self.agent_temporary_workspace.mkdir(parents=True, exist_ok=True)
700
+
701
+ # Copy all snapshots using anonymous IDs
702
+ for agent_id, snapshot_path in all_snapshots.items():
703
+ if snapshot_path.exists() and snapshot_path.is_dir():
704
+ # Use anonymous ID for destination directory
705
+ anon_id = agent_mapping.get(agent_id, agent_id)
706
+ dest_dir = self.agent_temporary_workspace / anon_id
707
+
708
+ # Copy snapshot content if not empty
709
+ if any(snapshot_path.iterdir()):
710
+ shutil.copytree(snapshot_path, dest_dir, dirs_exist_ok=True)
711
+
712
+ return self.agent_temporary_workspace
713
+
714
+ def _log_workspace_contents(self, workspace_path: Path, workspace_name: str, context: str = "") -> None:
715
+ """
716
+ Log the contents of a workspace directory for visibility.
717
+
718
+ Args:
719
+ workspace_path: Path to the workspace to log
720
+ workspace_name: Human-readable name for the workspace
721
+ context: Additional context (e.g., "before execution", "after execution")
722
+ """
723
+ if not workspace_path or not workspace_path.exists():
724
+ logger.info(f"[FilesystemManager.{workspace_name}] {context} - Workspace does not exist: {workspace_path}")
725
+ return
726
+
727
+ try:
728
+ files = list(workspace_path.rglob("*"))
729
+ file_paths = [str(f.relative_to(workspace_path)) for f in files if f.is_file()]
730
+ dir_paths = [str(f.relative_to(workspace_path)) for f in files if f.is_dir()]
731
+
732
+ logger.info(f"[FilesystemManager.{workspace_name}] {context} - Workspace: {workspace_path}")
733
+ if file_paths:
734
+ logger.info(f"[FilesystemManager.{workspace_name}] {context} - Files ({len(file_paths)}): {file_paths}")
735
+ if dir_paths:
736
+ logger.info(f"[FilesystemManager.{workspace_name}] {context} - Directories ({len(dir_paths)}): {dir_paths}")
737
+ if not file_paths and not dir_paths:
738
+ logger.info(f"[FilesystemManager.{workspace_name}] {context} - Empty workspace")
739
+ except Exception as e:
740
+ logger.warning(f"[FilesystemManager.{workspace_name}] {context} - Error reading workspace: {e}")
741
+
742
+ def log_current_state(self, context: str = "") -> None:
743
+ """
744
+ Log the current state of both main and temp workspaces.
745
+
746
+ Args:
747
+ context: Context for the logging (e.g., "before execution", "after answer")
748
+ """
749
+ agent_context = f"agent_id={self.agent_id}, {context}" if context else f"agent_id={self.agent_id}"
750
+
751
+ # Log main workspace
752
+ self._log_workspace_contents(self.get_current_workspace(), "main_workspace", agent_context)
753
+
754
+ # Log temp workspace if it exists
755
+ if self.agent_temporary_workspace:
756
+ self._log_workspace_contents(self.agent_temporary_workspace, "temp_workspace", agent_context)
757
+
758
+ def set_temporary_workspace(self, use_temporary: bool = True) -> None:
759
+ """
760
+ Switch between main workspace and temporary workspace.
761
+
762
+ Args:
763
+ use_temporary: If True, use temporary workspace; if False, use main workspace
764
+ """
765
+ self._using_temporary = use_temporary
766
+
767
+ # Update current working directory path
768
+ if use_temporary and self.agent_temporary_workspace:
769
+ self.cwd = self.agent_temporary_workspace
770
+ else:
771
+ self.cwd = self._original_cwd
772
+
773
+ def get_current_workspace(self) -> Path:
774
+ """
775
+ Get the current active workspace path.
776
+
777
+ Returns:
778
+ Path to the current workspace
779
+ """
780
+ return self.cwd
781
+
782
+ def cleanup(self) -> None:
783
+ """Cleanup temporary resources (not the main workspace) and Docker containers."""
784
+ # Cleanup Docker container if Docker mode enabled
785
+ if self.docker_manager and self.agent_id:
786
+ self.docker_manager.cleanup(self.agent_id)
787
+
788
+ # Cleanup temporary workspace
789
+ p = self.agent_temporary_workspace
790
+
791
+ # Aggressive path-checking for validity
792
+ if not p:
793
+ return
794
+ try:
795
+ p = p.resolve()
796
+ if not p.exists():
797
+ return
798
+ assert p.is_absolute(), "Temporary workspace must be absolute"
799
+ assert p.is_dir(), "Temporary workspace must be a directory"
800
+
801
+ if self.agent_temporary_workspace_parent:
802
+ parent = Path(self.agent_temporary_workspace_parent).resolve()
803
+ try:
804
+ p.relative_to(parent)
805
+ except ValueError:
806
+ raise AssertionError(f"Refusing to delete workspace outside of parent: {p}")
807
+
808
+ if p == Path("/") or len(p.parts) < 3:
809
+ raise AssertionError(f"Unsafe path for deletion: {p}")
810
+
811
+ shutil.rmtree(p)
812
+ except Exception as e:
813
+ logger.warning(f"[FilesystemManager] cleanup failed for {p}: {e}")