microbots 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. microbots-0.0.1/LICENSE +21 -0
  2. microbots-0.0.1/PKG-INFO +47 -0
  3. microbots-0.0.1/README.md +0 -0
  4. microbots-0.0.1/pyproject.toml +33 -0
  5. microbots-0.0.1/requirements.txt +9 -0
  6. microbots-0.0.1/setup.cfg +4 -0
  7. microbots-0.0.1/src/microbot/MicroBot.py +173 -0
  8. microbots-0.0.1/src/microbot/__init__.py +10 -0
  9. microbots-0.0.1/src/microbot/bot/BrowserBot.py +32 -0
  10. microbots-0.0.1/src/microbot/bot/CustomBot.py +31 -0
  11. microbots-0.0.1/src/microbot/bot/ReadingBot.py +41 -0
  12. microbots-0.0.1/src/microbot/bot/WritingBot.py +38 -0
  13. microbots-0.0.1/src/microbot/bot/__init__.py +4 -0
  14. microbots-0.0.1/src/microbot/constants.py +25 -0
  15. microbots-0.0.1/src/microbot/environment/Environment.py +22 -0
  16. microbots-0.0.1/src/microbot/environment/local_docker/LocalDockerEnvironment.py +156 -0
  17. microbots-0.0.1/src/microbot/environment/local_docker/__init__.py +1 -0
  18. microbots-0.0.1/src/microbot/environment/local_docker/image_builder/ShellCommunicator.py +261 -0
  19. microbots-0.0.1/src/microbot/environment/local_docker/image_builder/dockerShell.py +28 -0
  20. microbots-0.0.1/src/microbot/environment/swe-rex/LocalDocker.py +139 -0
  21. microbots-0.0.1/src/microbot/llm/__init__.py +0 -0
  22. microbots-0.0.1/src/microbot/llm/openai_api.py +78 -0
  23. microbots-0.0.1/src/microbot/tool/tool.py +74 -0
  24. microbots-0.0.1/src/microbot/tool/tool_definition/browser-use/browser.py +40 -0
  25. microbots-0.0.1/src/microbot/tool_definitions/base_tool.py +23 -0
  26. microbots-0.0.1/src/microbot/tool_definitions/ctags.py +25 -0
  27. microbots-0.0.1/src/microbot/tool_definitions/node.py +24 -0
  28. microbots-0.0.1/src/microbot/utils/logger.py +14 -0
  29. microbots-0.0.1/src/microbot/utils/network.py +16 -0
  30. microbots-0.0.1/src/microbots.egg-info/PKG-INFO +47 -0
  31. microbots-0.0.1/src/microbots.egg-info/SOURCES.txt +32 -0
  32. microbots-0.0.1/src/microbots.egg-info/dependency_links.txt +1 -0
  33. microbots-0.0.1/src/microbots.egg-info/requires.txt +9 -0
  34. microbots-0.0.1/src/microbots.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Bala
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: microbots
3
+ Version: 0.0.1
4
+ Summary: container-based autonomous agent framework
5
+ Author-email: xxx <xxx@example.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Bala
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Source Repo, https://github.com/microsoft/minions
29
+ Project-URL: Issues, https://github.com/microsoft/minions/issues
30
+ Keywords: agent,bot,micro
31
+ Classifier: Operating System :: OS Independent
32
+ Classifier: Intended Audience :: Developers
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3.11
35
+ Requires-Python: >=3.11
36
+ Description-Content-Type: text/markdown
37
+ License-File: LICENSE
38
+ Requires-Dist: openai==1.107.3
39
+ Requires-Dist: python-dotenv==1.1.1
40
+ Requires-Dist: docker==7.1.0
41
+ Requires-Dist: fastapi==0.116.1
42
+ Requires-Dist: uvicorn==0.35.0
43
+ Requires-Dist: pydantic==2.11.9
44
+ Requires-Dist: swe-rex==1.4.0
45
+ Requires-Dist: aiohttp==3.12.15
46
+ Requires-Dist: pyyaml==6.0.2
47
+ Dynamic: license-file
File without changes
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "microbots" # package name on PyPI (unique)
7
+ dynamic = ["version", "dependencies"]
8
+ description = "container-based autonomous agent framework"
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ keywords = ["agent", "bot", "micro"]
12
+ authors = [
13
+ { name = "xxx", email = "xxx@example.com" }
14
+ ]
15
+ classifiers = [
16
+ "Operating System :: OS Independent",
17
+ # Indicate who your project is intended for
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3.11",
21
+ ]
22
+ requires-python = ">=3.11"
23
+
24
+ [tool.setuptools.dynamic]
25
+ dependencies = { file = ["requirements.txt"] }
26
+ version = {attr ="microbot.__version__"}
27
+
28
+ [tool.setuptools.packages.find]
29
+ where = ["src"]
30
+
31
+ [project.urls]
32
+ "Source Repo" = "https://github.com/microsoft/minions"
33
+ Issues = "https://github.com/microsoft/minions/issues"
@@ -0,0 +1,9 @@
1
+ openai==1.107.3
2
+ python-dotenv==1.1.1
3
+ docker==7.1.0
4
+ fastapi==0.116.1
5
+ uvicorn==0.35.0
6
+ pydantic==2.11.9
7
+ swe-rex==1.4.0
8
+ aiohttp==3.12.15
9
+ pyyaml==6.0.2
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,173 @@
1
+ import json
2
+ import os
3
+ import time
4
+ from dataclasses import dataclass
5
+ from enum import StrEnum
6
+ from logging import getLogger
7
+ from typing import Optional
8
+
9
+ from microbot.constants import ModelProvider, PermissionLabels, PermissionMapping
10
+ from microbot.environment.local_docker.LocalDockerEnvironment import LocalDockerEnvironment
11
+ from microbot.llm.openai_api import OpenAIApi
12
+ from microbot.tool_definitions.base_tool import BaseTool
13
+ from microbot.utils.logger import LogLevelEmoji, dividerString
14
+ from microbot.utils.network import get_free_port
15
+
16
+ logger = getLogger(" MicroBot ")
17
+
18
+ llm_output_format = """```json
19
+ {
20
+ task_done: true | false,
21
+ command: "<command to run> | null",
22
+ result: str | null
23
+ }
24
+ ```
25
+ """
26
+
27
+ system_prompt_common = """There is a shell session open for you.
28
+ I will provide a task to achieve using the shell.
29
+ You will provide the commands to achieve the task in this particular below json format, Ensure all the time to respond in this format only and nothing else, also all the properties ( task_done, command, result ) are mandatory on each response
30
+ {llm_output_format}
31
+ after each command I will provide the output of the command.
32
+ ensure to run only one command at a time.
33
+ I won't be able to intervene once I have given task. ."""
34
+
35
+
36
+ class BotType(StrEnum):
37
+ READING_BOT = "READING_BOT"
38
+ WRITING_BOT = "WRITING_BOT"
39
+ BROWSING_BOT = "BROWSING_BOT"
40
+ CUSTOM_BOT = "CUSTOM_BOT"
41
+
42
+
43
+ @dataclass
44
+ class BotRunResult:
45
+ status: bool
46
+ result: str | None
47
+ error: Optional[str]
48
+
49
+
50
+ class MicroBot:
51
+
52
+ def __init__(
53
+ self,
54
+ bot_type: BotType,
55
+ model: str,
56
+ system_prompt: Optional[str] = None,
57
+ environment: Optional[any] = None,
58
+ additional_tools: Optional[list[BaseTool]] = [],
59
+ folder_to_mount: Optional[str] = None,
60
+ permission: Optional[PermissionLabels] = None,
61
+ ):
62
+ # validate init values before assigning
63
+ self.permission = permission
64
+ self.permission = permission
65
+ if folder_to_mount is not None:
66
+ self.folder_to_mount_base_path = os.path.basename(folder_to_mount) # TODO
67
+
68
+ self._validate_model_and_provider(model)
69
+ self._validate_model_and_provider(model)
70
+ self.permission_key = PermissionMapping.MAPPING.get(self.permission)
71
+ self.system_prompt = system_prompt
72
+ self.system_prompt = system_prompt
73
+ self.model = model
74
+ self.bot_type = bot_type
75
+ self.model_provider = model.split("/")[0]
76
+ self.deployment_name = model.split("/")[1]
77
+ self.environment = environment
78
+ self._create_environment(folder_to_mount)
79
+ self._create_llm()
80
+
81
+ def run(self, task, max_iterations=20, timeout_in_seconds=200) -> BotRunResult:
82
+
83
+ iteration_count = 1
84
+ # start timer
85
+ start_time = time.time()
86
+ timeout = timeout_in_seconds
87
+ llm_response = self.llm.ask(task)
88
+ return_value = BotRunResult(
89
+ status=False,
90
+ result=None,
91
+ error="Did not complete",
92
+ )
93
+ logger.info("%s TASK STARTED : %s...", LogLevelEmoji.INFO, task[0:15])
94
+ while llm_response.task_done is False:
95
+ print(dividerString)
96
+ logger.info(
97
+ " %s LLM Iteration Count : %d", LogLevelEmoji.INFO, iteration_count
98
+ )
99
+ logger.info(
100
+ " âžĄī¸ LLM tool call : %s",
101
+ json.dumps(llm_response.command),
102
+ )
103
+ # increment iteration count
104
+ iteration_count += 1
105
+ if iteration_count >= max_iterations:
106
+ return_value.error = f"Max iterations {max_iterations} reached"
107
+ return return_value
108
+
109
+ # check if timeout has reached
110
+ current_time = time.time()
111
+ elapsed_time = current_time - start_time
112
+
113
+ if elapsed_time > timeout:
114
+ logger.error(
115
+ "Iteration %d with response %s",
116
+ iteration_count,
117
+ json.dumps(llm_response),
118
+ )
119
+ return_value.error = f"Timeout of {timeout} seconds reached"
120
+ return return_value
121
+
122
+ llm_command_output = self.environment.execute(llm_response.command)
123
+ logger.info(
124
+ " âŦ…ī¸ Command Execution Output : %s",
125
+ llm_command_output,
126
+ )
127
+ # Convert CmdReturn to string for LLM
128
+ if llm_command_output.stdout:
129
+ output_text = llm_command_output.stdout
130
+ elif llm_command_output.stderr:
131
+ output_text = f"COMMUNICATION ERROR: {llm_command_output.stderr}"
132
+ else:
133
+ output_text = "No output received"
134
+
135
+ llm_response = self.llm.ask(output_text)
136
+
137
+ logger.info("🔚 TASK COMPLETED : %s...", task[0:15])
138
+ return BotRunResult(status=True, result=llm_response.result, error=None)
139
+
140
+ def _create_environment(self, folder_to_mount):
141
+ if self.environment is None:
142
+ # check for a free port in the system and assign to environment
143
+
144
+ free_port = get_free_port()
145
+
146
+ self.environment = LocalDockerEnvironment(
147
+ port=free_port,
148
+ folder_to_mount=folder_to_mount,
149
+ permission=self.permission,
150
+ )
151
+
152
+ def _create_llm(self):
153
+ if self.model_provider == ModelProvider.OPENAI:
154
+ self.llm = OpenAIApi(
155
+ system_prompt=self.system_prompt, deployment_name=self.deployment_name
156
+ )
157
+
158
+ def _validate_model_and_provider(self, model):
159
+ # Ensure it has only only slash
160
+ if model.count("/") != 1:
161
+ raise ValueError("Model should be in the format <provider>/<model_name>")
162
+ provider = model.split("/")[0]
163
+ if provider not in [e.value for e in ModelProvider]:
164
+ raise ValueError(f"Unsupported model provider: {provider}")
165
+
166
+ def __del__(self):
167
+ if self.environment:
168
+ try:
169
+ self.environment.stop()
170
+ except Exception as e:
171
+ logger.error(
172
+ "%s Error while stopping environment: %s", LogLevelEmoji.ERROR, e
173
+ )
@@ -0,0 +1,10 @@
1
+ from microbot.bot import ReadingBot, WritingBot, BrowserBot, CustomBot
2
+
3
+ __all__ = [
4
+ "ReadingBot",
5
+ "WritingBot",
6
+ "BrowserBot",
7
+ "CustomBot"
8
+ ]
9
+
10
+ __version__ = "0.0.1"
@@ -0,0 +1,32 @@
1
+ import os
2
+ from typing import Optional
3
+
4
+ from microbot.constants import PermissionLabels
5
+ from microbot.MicroBot import BotType, MicroBot, system_prompt_common
6
+ from microbot.tool_definitions.base_tool import BaseTool
7
+
8
+
9
+ class BrowserBot(MicroBot):
10
+
11
+ def __init__(
12
+ self,
13
+ model: str,
14
+ environment: Optional[any] = None,
15
+ additional_tools: Optional[list[BaseTool]] = [],
16
+ ):
17
+ # validate init values before assigning
18
+ bot_type = BotType.BROWSING_BOT
19
+ permission = PermissionLabels.READ_WRITE
20
+ system_prompt = f"""
21
+ {system_prompt_common}
22
+ You are also provided access to internet to search for information.
23
+ """
24
+
25
+ super().__init__(
26
+ bot_type,
27
+ model,
28
+ system_prompt,
29
+ environment,
30
+ additional_tools,
31
+ permission,
32
+ )
@@ -0,0 +1,31 @@
1
+ import os
2
+ from typing import Optional
3
+
4
+ from microbot.constants import PermissionLabels
5
+ from microbot.MicroBot import BotType, MicroBot
6
+ from microbot.tool_definitions.base_tool import BaseTool
7
+
8
+
9
+ class BrowserBot(MicroBot):
10
+
11
+ def __init__(
12
+ self,
13
+ model: str,
14
+ system_prompt: str,
15
+ folder_to_mount: Optional[str] = None,
16
+ environment: Optional[any] = None,
17
+ additional_tools: Optional[list[BaseTool]] = [],
18
+ ):
19
+ # validate init values before assigning
20
+ bot_type = BotType.BROWSING_BOT
21
+ permission = PermissionLabels.READ_WRITE
22
+
23
+ super().__init__(
24
+ bot_type,
25
+ model,
26
+ system_prompt,
27
+ environment,
28
+ additional_tools,
29
+ folder_to_mount,
30
+ permission,
31
+ )
@@ -0,0 +1,41 @@
1
+ from pathlib import Path
2
+ from typing import Optional
3
+
4
+ from microbot.constants import DOCKER_WORKING_DIR, PermissionLabels
5
+ from microbot.MicroBot import BotType, MicroBot, system_prompt_common
6
+ from microbot.tool_definitions.base_tool import BaseTool
7
+
8
+
9
+ class ReadingBot(MicroBot):
10
+
11
+ def __init__(
12
+ self,
13
+ model: str,
14
+ folder_to_mount: str,
15
+ environment: Optional[any] = None,
16
+ additional_tools: Optional[list[BaseTool]] = [],
17
+ ):
18
+ # validate init values before assigning
19
+ bot_type = BotType.READING_BOT
20
+ permission = PermissionLabels.READ_ONLY
21
+
22
+ base_name = Path(folder_to_mount).name
23
+
24
+ system_prompt = f"""
25
+ {system_prompt_common}
26
+ You are a reading bot.
27
+ You are only provided access to read files inside the mounted directory.
28
+ The directory is mounted at /{DOCKER_WORKING_DIR}/{base_name} in your current environment.
29
+ You can access files using paths like /{DOCKER_WORKING_DIR}/{base_name}/filename.txt or by changing to that directory first.
30
+ Once all the commands are done, and task is verified finally give me the result.
31
+ """
32
+
33
+ super().__init__(
34
+ bot_type,
35
+ model,
36
+ system_prompt,
37
+ environment,
38
+ additional_tools,
39
+ folder_to_mount,
40
+ permission,
41
+ )
@@ -0,0 +1,38 @@
1
+ from typing import Optional
2
+
3
+ from microbot.constants import PermissionLabels
4
+ from microbot.MicroBot import BotType, MicroBot, system_prompt_common
5
+ from microbot.tool_definitions.base_tool import BaseTool
6
+
7
+
8
+ class WritingBot(MicroBot):
9
+
10
+ def __init__(
11
+ self,
12
+ model: str,
13
+ folder_to_mount: str,
14
+ environment: Optional[any] = None,
15
+ additional_tools: Optional[list[BaseTool]] = [],
16
+ ):
17
+ # validate init values before assigning
18
+ bot_type = BotType.WRITING_BOT
19
+ permission = PermissionLabels.READ_WRITE
20
+
21
+ system_prompt = f"""
22
+ {system_prompt_common}
23
+ You are a writing bot.
24
+ You are only provided access to write files inside the mounted directory.
25
+ The directory is mounted at /app/{folder_to_mount} in your current environment.
26
+ You can access files using paths like /app/{folder_to_mount}/filename.txt or by changing to that directory first.
27
+ Once all the commands are done, and task is verified finally give me the result.
28
+ """
29
+
30
+ super().__init__(
31
+ bot_type,
32
+ model,
33
+ system_prompt,
34
+ environment,
35
+ additional_tools,
36
+ folder_to_mount,
37
+ permission,
38
+ )
@@ -0,0 +1,4 @@
1
+ from .ReadingBot import ReadingBot
2
+ from .WritingBot import WritingBot
3
+
4
+ __all__ = ["ReadingBot", "WritingBot"]
@@ -0,0 +1,25 @@
1
+ from enum import Enum, StrEnum
2
+ from typing import Optional, TypedDict
3
+
4
+
5
+ class ModelProvider(StrEnum):
6
+ OPENAI = "openai"
7
+
8
+
9
+ class ModelEnum(StrEnum):
10
+ GPT_5 = "gpt-5"
11
+
12
+
13
+ class PermissionLabels(StrEnum):
14
+ READ_ONLY = "READ_ONLY"
15
+ READ_WRITE = "READ_WRITE"
16
+
17
+
18
+ class PermissionMapping:
19
+ MAPPING = {
20
+ PermissionLabels.READ_ONLY: "ro",
21
+ PermissionLabels.READ_WRITE: "rw",
22
+ }
23
+
24
+
25
+ DOCKER_WORKING_DIR = "workdir"
@@ -0,0 +1,22 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Optional
3
+
4
+ class CmdReturn:
5
+ def __init__(self, stdout: str, stderr: str, return_code: int):
6
+ self.stdout = stdout
7
+ self.stderr = stderr
8
+ self.return_code = return_code
9
+
10
+
11
+ class Environment(ABC):
12
+ @abstractmethod
13
+ def start(self):
14
+ pass
15
+
16
+ @abstractmethod
17
+ def stop(self):
18
+ pass
19
+
20
+ @abstractmethod
21
+ def execute(self, command: str, timeout: Optional[int] = 300) -> CmdReturn:
22
+ pass
@@ -0,0 +1,156 @@
1
+ import logging
2
+ import os
3
+ import time
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from microbot.environment.Environment import Environment, CmdReturn
8
+
9
+ import docker
10
+ import requests
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ WORKING_DIR = str(Path.home() / "MICROBOT_WORKDIR")
15
+ DOCKER_WORKING_DIR = "/workdir"
16
+
17
+
18
+ class LocalDockerEnvironment(Environment):
19
+ def __init__(
20
+ self,
21
+ port: int,
22
+ folder_to_mount: Optional[str] = None,
23
+ permission: Optional[str] = None,
24
+ image: str = "kavyasree261002/shell_server:latest",
25
+ ):
26
+ if folder_to_mount is None and permission is not None:
27
+ raise ValueError("permission provided but folder_to_mount is None")
28
+ elif permission is None and folder_to_mount is not None:
29
+ raise ValueError("folder_to_mount provided but permission is None")
30
+ if permission is not None and permission not in ["READ_ONLY", "READ_WRITE"]:
31
+ raise ValueError(
32
+ "permission must be 'READ_ONLY' or 'READ_WRITE' when provided"
33
+ )
34
+
35
+ self.image = image
36
+ self.folder_to_mount = folder_to_mount
37
+ self.permission = permission
38
+ self.container = None
39
+ self.client = docker.from_env()
40
+ self.port = port # required host port
41
+ self.container_port = 8080
42
+ self._create_working_dir()
43
+ self.start()
44
+
45
+ def _create_working_dir(self):
46
+ if not os.path.exists(WORKING_DIR):
47
+ os.makedirs(WORKING_DIR)
48
+ logger.info("đŸ—‚ī¸ Created working directory at %s", WORKING_DIR)
49
+ else:
50
+ logger.info("đŸ—‚ī¸ Working directory already exists at %s", WORKING_DIR)
51
+
52
+ def start(self):
53
+ mode_map = {"READ_ONLY": "ro", "READ_WRITE": "rw"}
54
+ volumes_config = {WORKING_DIR: {"bind": DOCKER_WORKING_DIR, "mode": "rw"}}
55
+ if self.folder_to_mount and self.permission:
56
+ if self.permission == "READ_ONLY":
57
+ volumes_config[self.folder_to_mount] = {
58
+ "bind": f"/ro/{os.path.basename(self.folder_to_mount)}",
59
+ "mode": mode_map[self.permission],
60
+ }
61
+ logger.info(
62
+ "đŸ“Ļ Volume mapping: %s → /ro/%s",
63
+ self.folder_to_mount,
64
+ os.path.basename(self.folder_to_mount),
65
+ )
66
+ else:
67
+ volumes_config[self.folder_to_mount] = {
68
+ "bind": f"/{DOCKER_WORKING_DIR}/{os.path.basename(self.folder_to_mount)}",
69
+ "mode": mode_map[self.permission],
70
+ }
71
+ logger.debug(
72
+ "đŸ“Ļ Volume mapping: %s → /{DOCKER_WORKING_DIR}/%s",
73
+ self.folder_to_mount,
74
+ os.path.basename(self.folder_to_mount),
75
+ )
76
+
77
+ # Port mapping
78
+ port_mapping = {f"{self.container_port}/tcp": self.port}
79
+
80
+ self.container = self.client.containers.run(
81
+ self.image,
82
+ volumes=volumes_config,
83
+ ports=port_mapping,
84
+ detach=True,
85
+ working_dir="/app",
86
+ privileged=True, # Required for mounting overlayfs
87
+ environment={"BOT_PORT": str(self.container_port)},
88
+ )
89
+ logger.info(
90
+ "🚀 Started container %s with image %s on host port %s",
91
+ self.container.id[:12],
92
+ self.image,
93
+ self.port,
94
+ )
95
+ time.sleep(2) # Give some time for the server to start
96
+
97
+ if self.permission == "READ_ONLY":
98
+ self._setup_overlay_mount(self.folder_to_mount)
99
+
100
+ def _setup_overlay_mount(self, folder_to_mount: str):
101
+ path_name = os.path.basename(os.path.abspath(folder_to_mount))
102
+ # Mount /ro/path_name to /{WORKING_DIR}/path_name using overlayfs
103
+ mount_command = (
104
+ f"mkdir -p /overlaydir && "
105
+ f"mkdir -p /{DOCKER_WORKING_DIR}/{path_name} /{DOCKER_WORKING_DIR}/overlay/{path_name}/upper /{DOCKER_WORKING_DIR}/overlay/{path_name}/work && "
106
+ f"mount -t overlay overlay -o lowerdir=/ro/{path_name},upperdir=/{DOCKER_WORKING_DIR}/overlay/{path_name}/upper,workdir=/{DOCKER_WORKING_DIR}/overlay/{path_name}/work /{DOCKER_WORKING_DIR}/{path_name}"
107
+ )
108
+ self.execute(mount_command)
109
+ logger.info(
110
+ "🔒 Set up overlay mount for read-only directory at /{DOCKER_WORKING_DIR}/%s",
111
+ path_name,
112
+ )
113
+
114
+ def stop(self):
115
+ """Stop and remove the container"""
116
+ if self.container:
117
+ self.container.stop()
118
+ self.container.remove()
119
+ self.container = None
120
+
121
+ # Remove working directory
122
+ if os.path.exists(WORKING_DIR):
123
+ try:
124
+ import shutil
125
+
126
+ shutil.rmtree(WORKING_DIR)
127
+ logger.info("đŸ—‘ī¸ Removed working directory at %s", WORKING_DIR)
128
+ except Exception as e:
129
+ logger.error("❌ Failed to remove working directory: %s", e)
130
+
131
+ def execute(self, command: str, timeout: Optional[int] = 10) -> CmdReturn: # TODO: Need proper return value
132
+ logger.debug("âžĄī¸ Executing command in container: %s", command)
133
+ try:
134
+ response = requests.post(
135
+ f"http://localhost:{self.port}/",
136
+ json={"message": command},
137
+ timeout=timeout,
138
+ )
139
+ response.raise_for_status()
140
+ logger.debug("âŦ…ī¸ Command output: %s", response.json().get("output", ""))
141
+ output = response.json().get("output", "")
142
+ return CmdReturn(
143
+ stdout=output, stderr="", return_code=0
144
+ )
145
+ self.container.reload()
146
+ logger.info("â„šī¸ Container status: %s", self.container.status)
147
+ if self.container.status != "running":
148
+ logs = self.container.logs().decode("utf-8", errors="replace")
149
+ logger.error("🛑 Container not running. Recent logs below:\n%s", logs)
150
+ return CmdReturn(stdout="", stderr="Connection error", return_code=1)
151
+ except requests.exceptions.RequestException as e:
152
+ logger.exception("❌ Request failed while executing command: %s", e)
153
+ return CmdReturn(stdout="", stderr=str(e), return_code=1)
154
+ except Exception as e:
155
+ logger.exception("❌ Unexpected error while executing command: %s", e)
156
+ return CmdReturn(stdout="", stderr="Unexpected error", return_code=1)
@@ -0,0 +1 @@
1
+ from .LocalDockerEnvironment import LocalDockerEnvironment