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.
- massgen/__init__.py +142 -8
- massgen/adapters/__init__.py +29 -0
- massgen/adapters/ag2_adapter.py +483 -0
- massgen/adapters/base.py +183 -0
- massgen/adapters/tests/__init__.py +0 -0
- massgen/adapters/tests/test_ag2_adapter.py +439 -0
- massgen/adapters/tests/test_agent_adapter.py +128 -0
- massgen/adapters/utils/__init__.py +2 -0
- massgen/adapters/utils/ag2_utils.py +236 -0
- massgen/adapters/utils/tests/__init__.py +0 -0
- massgen/adapters/utils/tests/test_ag2_utils.py +138 -0
- massgen/agent_config.py +329 -55
- massgen/api_params_handler/__init__.py +10 -0
- massgen/api_params_handler/_api_params_handler_base.py +99 -0
- massgen/api_params_handler/_chat_completions_api_params_handler.py +176 -0
- massgen/api_params_handler/_claude_api_params_handler.py +113 -0
- massgen/api_params_handler/_response_api_params_handler.py +130 -0
- massgen/backend/__init__.py +39 -4
- massgen/backend/azure_openai.py +385 -0
- massgen/backend/base.py +341 -69
- massgen/backend/base_with_mcp.py +1102 -0
- massgen/backend/capabilities.py +386 -0
- massgen/backend/chat_completions.py +577 -130
- massgen/backend/claude.py +1033 -537
- massgen/backend/claude_code.py +1203 -0
- massgen/backend/cli_base.py +209 -0
- massgen/backend/docs/BACKEND_ARCHITECTURE.md +126 -0
- massgen/backend/{CLAUDE_API_RESEARCH.md → docs/CLAUDE_API_RESEARCH.md} +18 -18
- massgen/backend/{GEMINI_API_DOCUMENTATION.md → docs/GEMINI_API_DOCUMENTATION.md} +9 -9
- massgen/backend/docs/Gemini MCP Integration Analysis.md +1050 -0
- massgen/backend/docs/MCP_IMPLEMENTATION_CLAUDE_BACKEND.md +177 -0
- massgen/backend/docs/MCP_INTEGRATION_RESPONSE_BACKEND.md +352 -0
- massgen/backend/docs/OPENAI_GPT5_MODELS.md +211 -0
- massgen/backend/{OPENAI_RESPONSES_API_FORMAT.md → docs/OPENAI_RESPONSE_API_TOOL_CALLS.md} +3 -3
- massgen/backend/docs/OPENAI_response_streaming.md +20654 -0
- massgen/backend/docs/inference_backend.md +257 -0
- massgen/backend/docs/permissions_and_context_files.md +1085 -0
- massgen/backend/external.py +126 -0
- massgen/backend/gemini.py +1850 -241
- massgen/backend/grok.py +40 -156
- massgen/backend/inference.py +156 -0
- massgen/backend/lmstudio.py +171 -0
- massgen/backend/response.py +1095 -322
- massgen/chat_agent.py +131 -113
- massgen/cli.py +1560 -275
- massgen/config_builder.py +2396 -0
- massgen/configs/BACKEND_CONFIGURATION.md +458 -0
- massgen/configs/README.md +559 -216
- massgen/configs/ag2/ag2_case_study.yaml +27 -0
- massgen/configs/ag2/ag2_coder.yaml +34 -0
- massgen/configs/ag2/ag2_coder_case_study.yaml +36 -0
- massgen/configs/ag2/ag2_gemini.yaml +27 -0
- massgen/configs/ag2/ag2_groupchat.yaml +108 -0
- massgen/configs/ag2/ag2_groupchat_gpt.yaml +118 -0
- massgen/configs/ag2/ag2_single_agent.yaml +21 -0
- massgen/configs/basic/multi/fast_timeout_example.yaml +37 -0
- massgen/configs/basic/multi/gemini_4o_claude.yaml +31 -0
- massgen/configs/basic/multi/gemini_gpt5nano_claude.yaml +36 -0
- massgen/configs/{gemini_4o_claude.yaml → basic/multi/geminicode_4o_claude.yaml} +3 -3
- massgen/configs/basic/multi/geminicode_gpt5nano_claude.yaml +36 -0
- massgen/configs/basic/multi/glm_gemini_claude.yaml +25 -0
- massgen/configs/basic/multi/gpt4o_audio_generation.yaml +30 -0
- massgen/configs/basic/multi/gpt4o_image_generation.yaml +31 -0
- massgen/configs/basic/multi/gpt5nano_glm_qwen.yaml +26 -0
- massgen/configs/basic/multi/gpt5nano_image_understanding.yaml +26 -0
- massgen/configs/{three_agents_default.yaml → basic/multi/three_agents_default.yaml} +8 -4
- massgen/configs/basic/multi/three_agents_opensource.yaml +27 -0
- massgen/configs/basic/multi/three_agents_vllm.yaml +20 -0
- massgen/configs/basic/multi/two_agents_gemini.yaml +19 -0
- massgen/configs/{two_agents.yaml → basic/multi/two_agents_gpt5.yaml} +14 -6
- massgen/configs/basic/multi/two_agents_opensource_lmstudio.yaml +31 -0
- massgen/configs/basic/multi/two_qwen_vllm_sglang.yaml +28 -0
- massgen/configs/{single_agent.yaml → basic/single/single_agent.yaml} +1 -1
- massgen/configs/{single_flash2.5.yaml → basic/single/single_flash2.5.yaml} +1 -2
- massgen/configs/basic/single/single_gemini2.5pro.yaml +16 -0
- massgen/configs/basic/single/single_gpt4o_audio_generation.yaml +22 -0
- massgen/configs/basic/single/single_gpt4o_image_generation.yaml +22 -0
- massgen/configs/basic/single/single_gpt4o_video_generation.yaml +24 -0
- massgen/configs/basic/single/single_gpt5nano.yaml +20 -0
- massgen/configs/basic/single/single_gpt5nano_file_search.yaml +18 -0
- massgen/configs/basic/single/single_gpt5nano_image_understanding.yaml +17 -0
- massgen/configs/basic/single/single_gptoss120b.yaml +15 -0
- massgen/configs/basic/single/single_openrouter_audio_understanding.yaml +15 -0
- massgen/configs/basic/single/single_qwen_video_understanding.yaml +15 -0
- massgen/configs/debug/code_execution/command_filtering_blacklist.yaml +29 -0
- massgen/configs/debug/code_execution/command_filtering_whitelist.yaml +28 -0
- massgen/configs/debug/code_execution/docker_verification.yaml +29 -0
- massgen/configs/debug/skip_coordination_test.yaml +27 -0
- massgen/configs/debug/test_sdk_migration.yaml +17 -0
- massgen/configs/docs/DISCORD_MCP_SETUP.md +208 -0
- massgen/configs/docs/TWITTER_MCP_ENESCINAR_SETUP.md +82 -0
- massgen/configs/providers/azure/azure_openai_multi.yaml +21 -0
- massgen/configs/providers/azure/azure_openai_single.yaml +19 -0
- massgen/configs/providers/claude/claude.yaml +14 -0
- massgen/configs/providers/gemini/gemini_gpt5nano.yaml +28 -0
- massgen/configs/providers/local/lmstudio.yaml +11 -0
- massgen/configs/providers/openai/gpt5.yaml +46 -0
- massgen/configs/providers/openai/gpt5_nano.yaml +46 -0
- massgen/configs/providers/others/grok_single_agent.yaml +19 -0
- massgen/configs/providers/others/zai_coding_team.yaml +108 -0
- massgen/configs/providers/others/zai_glm45.yaml +12 -0
- massgen/configs/{creative_team.yaml → teams/creative/creative_team.yaml} +16 -6
- massgen/configs/{travel_planning.yaml → teams/creative/travel_planning.yaml} +16 -6
- massgen/configs/{news_analysis.yaml → teams/research/news_analysis.yaml} +16 -6
- massgen/configs/{research_team.yaml → teams/research/research_team.yaml} +15 -7
- massgen/configs/{technical_analysis.yaml → teams/research/technical_analysis.yaml} +16 -6
- massgen/configs/tools/code-execution/basic_command_execution.yaml +25 -0
- massgen/configs/tools/code-execution/code_execution_use_case_simple.yaml +41 -0
- massgen/configs/tools/code-execution/docker_claude_code.yaml +32 -0
- massgen/configs/tools/code-execution/docker_multi_agent.yaml +32 -0
- massgen/configs/tools/code-execution/docker_simple.yaml +29 -0
- massgen/configs/tools/code-execution/docker_with_resource_limits.yaml +32 -0
- massgen/configs/tools/code-execution/multi_agent_playwright_automation.yaml +57 -0
- massgen/configs/tools/filesystem/cc_gpt5_gemini_filesystem.yaml +34 -0
- massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +68 -0
- massgen/configs/tools/filesystem/claude_code_flash2.5.yaml +43 -0
- massgen/configs/tools/filesystem/claude_code_flash2.5_gptoss.yaml +49 -0
- massgen/configs/tools/filesystem/claude_code_gpt5nano.yaml +31 -0
- massgen/configs/tools/filesystem/claude_code_single.yaml +40 -0
- massgen/configs/tools/filesystem/fs_permissions_test.yaml +87 -0
- massgen/configs/tools/filesystem/gemini_gemini_workspace_cleanup.yaml +54 -0
- massgen/configs/tools/filesystem/gemini_gpt5_filesystem_casestudy.yaml +30 -0
- massgen/configs/tools/filesystem/gemini_gpt5nano_file_context_path.yaml +43 -0
- massgen/configs/tools/filesystem/gemini_gpt5nano_protected_paths.yaml +45 -0
- massgen/configs/tools/filesystem/gpt5mini_cc_fs_context_path.yaml +31 -0
- massgen/configs/tools/filesystem/grok4_gpt5_gemini_filesystem.yaml +32 -0
- massgen/configs/tools/filesystem/multiturn/grok4_gpt5_claude_code_filesystem_multiturn.yaml +58 -0
- massgen/configs/tools/filesystem/multiturn/grok4_gpt5_gemini_filesystem_multiturn.yaml +58 -0
- massgen/configs/tools/filesystem/multiturn/two_claude_code_filesystem_multiturn.yaml +47 -0
- massgen/configs/tools/filesystem/multiturn/two_gemini_flash_filesystem_multiturn.yaml +48 -0
- massgen/configs/tools/mcp/claude_code_discord_mcp_example.yaml +27 -0
- massgen/configs/tools/mcp/claude_code_simple_mcp.yaml +35 -0
- massgen/configs/tools/mcp/claude_code_twitter_mcp_example.yaml +32 -0
- massgen/configs/tools/mcp/claude_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/claude_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/five_agents_travel_mcp_test.yaml +157 -0
- massgen/configs/tools/mcp/five_agents_weather_mcp_test.yaml +103 -0
- massgen/configs/tools/mcp/gemini_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test.yaml +23 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_sharing.yaml +23 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_single_agent.yaml +17 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_with_claude_code.yaml +24 -0
- massgen/configs/tools/mcp/gemini_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/gemini_notion_mcp.yaml +52 -0
- massgen/configs/tools/mcp/gpt5_nano_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/gpt5_nano_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/gpt5mini_claude_code_discord_mcp_example.yaml +38 -0
- massgen/configs/tools/mcp/gpt_oss_mcp_example.yaml +25 -0
- massgen/configs/tools/mcp/gpt_oss_mcp_test.yaml +28 -0
- massgen/configs/tools/mcp/grok3_mini_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/grok3_mini_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/multimcp_gemini.yaml +111 -0
- massgen/configs/tools/mcp/qwen_api_mcp_example.yaml +25 -0
- massgen/configs/tools/mcp/qwen_api_mcp_test.yaml +28 -0
- massgen/configs/tools/mcp/qwen_local_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/qwen_local_mcp_test.yaml +27 -0
- massgen/configs/tools/planning/five_agents_discord_mcp_planning_mode.yaml +140 -0
- massgen/configs/tools/planning/five_agents_filesystem_mcp_planning_mode.yaml +151 -0
- massgen/configs/tools/planning/five_agents_notion_mcp_planning_mode.yaml +151 -0
- massgen/configs/tools/planning/five_agents_twitter_mcp_planning_mode.yaml +155 -0
- massgen/configs/tools/planning/gpt5_mini_case_study_mcp_planning_mode.yaml +73 -0
- massgen/configs/tools/web-search/claude_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gemini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gpt5_mini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gpt_oss_streamable_http_test.yaml +44 -0
- massgen/configs/tools/web-search/grok3_mini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/qwen_api_streamable_http_test.yaml +44 -0
- massgen/configs/tools/web-search/qwen_local_streamable_http_test.yaml +43 -0
- massgen/coordination_tracker.py +708 -0
- massgen/docker/README.md +462 -0
- massgen/filesystem_manager/__init__.py +21 -0
- massgen/filesystem_manager/_base.py +9 -0
- massgen/filesystem_manager/_code_execution_server.py +545 -0
- massgen/filesystem_manager/_docker_manager.py +477 -0
- massgen/filesystem_manager/_file_operation_tracker.py +248 -0
- massgen/filesystem_manager/_filesystem_manager.py +813 -0
- massgen/filesystem_manager/_path_permission_manager.py +1261 -0
- massgen/filesystem_manager/_workspace_tools_server.py +1815 -0
- massgen/formatter/__init__.py +10 -0
- massgen/formatter/_chat_completions_formatter.py +284 -0
- massgen/formatter/_claude_formatter.py +235 -0
- massgen/formatter/_formatter_base.py +156 -0
- massgen/formatter/_response_formatter.py +263 -0
- massgen/frontend/__init__.py +1 -2
- massgen/frontend/coordination_ui.py +471 -286
- massgen/frontend/displays/base_display.py +56 -11
- massgen/frontend/displays/create_coordination_table.py +1956 -0
- massgen/frontend/displays/rich_terminal_display.py +1259 -619
- massgen/frontend/displays/simple_display.py +9 -4
- massgen/frontend/displays/terminal_display.py +27 -68
- massgen/logger_config.py +681 -0
- massgen/mcp_tools/README.md +232 -0
- massgen/mcp_tools/__init__.py +105 -0
- massgen/mcp_tools/backend_utils.py +1035 -0
- massgen/mcp_tools/circuit_breaker.py +195 -0
- massgen/mcp_tools/client.py +894 -0
- massgen/mcp_tools/config_validator.py +138 -0
- massgen/mcp_tools/docs/circuit_breaker.md +646 -0
- massgen/mcp_tools/docs/client.md +950 -0
- massgen/mcp_tools/docs/config_validator.md +478 -0
- massgen/mcp_tools/docs/exceptions.md +1165 -0
- massgen/mcp_tools/docs/security.md +854 -0
- massgen/mcp_tools/exceptions.py +338 -0
- massgen/mcp_tools/hooks.py +212 -0
- massgen/mcp_tools/security.py +780 -0
- massgen/message_templates.py +342 -64
- massgen/orchestrator.py +1515 -241
- massgen/stream_chunk/__init__.py +35 -0
- massgen/stream_chunk/base.py +92 -0
- massgen/stream_chunk/multimodal.py +237 -0
- massgen/stream_chunk/text.py +162 -0
- massgen/tests/mcp_test_server.py +150 -0
- massgen/tests/multi_turn_conversation_design.md +0 -8
- massgen/tests/test_azure_openai_backend.py +156 -0
- massgen/tests/test_backend_capabilities.py +262 -0
- massgen/tests/test_backend_event_loop_all.py +179 -0
- massgen/tests/test_chat_completions_refactor.py +142 -0
- massgen/tests/test_claude_backend.py +15 -28
- massgen/tests/test_claude_code.py +268 -0
- massgen/tests/test_claude_code_context_sharing.py +233 -0
- massgen/tests/test_claude_code_orchestrator.py +175 -0
- massgen/tests/test_cli_backends.py +180 -0
- massgen/tests/test_code_execution.py +679 -0
- massgen/tests/test_external_agent_backend.py +134 -0
- massgen/tests/test_final_presentation_fallback.py +237 -0
- massgen/tests/test_gemini_planning_mode.py +351 -0
- massgen/tests/test_grok_backend.py +7 -10
- massgen/tests/test_http_mcp_server.py +42 -0
- massgen/tests/test_integration_simple.py +198 -0
- massgen/tests/test_mcp_blocking.py +125 -0
- massgen/tests/test_message_context_building.py +29 -47
- massgen/tests/test_orchestrator_final_presentation.py +48 -0
- massgen/tests/test_path_permission_manager.py +2087 -0
- massgen/tests/test_rich_terminal_display.py +14 -13
- massgen/tests/test_timeout.py +133 -0
- massgen/tests/test_v3_3agents.py +11 -12
- massgen/tests/test_v3_simple.py +8 -13
- massgen/tests/test_v3_three_agents.py +11 -18
- massgen/tests/test_v3_two_agents.py +8 -13
- massgen/token_manager/__init__.py +7 -0
- massgen/token_manager/token_manager.py +400 -0
- massgen/utils.py +52 -16
- massgen/v1/agent.py +45 -91
- massgen/v1/agents.py +18 -53
- massgen/v1/backends/gemini.py +50 -153
- massgen/v1/backends/grok.py +21 -54
- massgen/v1/backends/oai.py +39 -111
- massgen/v1/cli.py +36 -93
- massgen/v1/config.py +8 -12
- massgen/v1/logging.py +43 -127
- massgen/v1/main.py +18 -32
- massgen/v1/orchestrator.py +68 -209
- massgen/v1/streaming_display.py +62 -163
- massgen/v1/tools.py +8 -12
- massgen/v1/types.py +9 -23
- massgen/v1/utils.py +5 -23
- massgen-0.1.0.dist-info/METADATA +1245 -0
- massgen-0.1.0.dist-info/RECORD +273 -0
- massgen-0.1.0.dist-info/entry_points.txt +2 -0
- massgen/frontend/logging/__init__.py +0 -9
- massgen/frontend/logging/realtime_logger.py +0 -197
- massgen-0.0.3.dist-info/METADATA +0 -568
- massgen-0.0.3.dist-info/RECORD +0 -76
- massgen-0.0.3.dist-info/entry_points.txt +0 -2
- /massgen/backend/{Function calling openai responses.md → docs/Function calling openai responses.md} +0 -0
- {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/WHEEL +0 -0
- {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/licenses/LICENSE +0 -0
- {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}")
|