feat: enhance README and improve environment configuration

- Added a new section in the README for Inference Providers, detailing setup instructions for Nous Portal, OpenRouter, and Custom Endpoints, improving user guidance for LLM connections.
- Updated messaging platform setup instructions to include Slack and WhatsApp, providing clearer steps for configuration.
- Introduced a new environment variable, TERMINAL_SANDBOX_DIR, to allow users to customize the sandbox storage location for Docker and Singularity environments.
- Refactored the Docker and Singularity environment classes to utilize the new sandbox directory for persistent workspaces, enhancing organization and usability.
- Improved handling of working directories across various environments, ensuring compatibility and clarity in execution paths.
This commit is contained in:
teknium1 2026-02-23 21:15:35 -08:00
parent 54dd1b3038
commit a183827128
7 changed files with 157 additions and 105 deletions

View file

@ -44,7 +44,7 @@ class DockerEnvironment(BaseEnvironment):
def __init__(
self,
image: str,
cwd: str = "/",
cwd: str = "~",
timeout: int = 60,
cpu: float = 0,
memory: int = 0,
@ -72,23 +72,26 @@ class DockerEnvironment(BaseEnvironment):
if not network:
resource_args.append("--network=none")
# Persistent volume for writable workspace that survives container restarts.
# Non-persistent mode uses tmpfs (ephemeral, fast, gone on cleanup).
self._volume_name: Optional[str] = None
# Persistent workspace via bind mounts from a configurable host directory
# (TERMINAL_SANDBOX_DIR, default ~/.hermes/sandboxes/). Non-persistent
# mode uses tmpfs (ephemeral, fast, gone on cleanup).
from tools.environments.base import get_sandbox_dir
self._workspace_dir: Optional[str] = None
self._home_dir: Optional[str] = None
if self._persistent:
self._volume_name = f"hermes-workspace-{task_id}"
# Create volume if it doesn't exist
subprocess.run(
["docker", "volume", "create", self._volume_name],
capture_output=True, timeout=10,
)
sandbox = get_sandbox_dir() / "docker" / task_id
self._workspace_dir = str(sandbox / "workspace")
self._home_dir = str(sandbox / "home")
os.makedirs(self._workspace_dir, exist_ok=True)
os.makedirs(self._home_dir, exist_ok=True)
writable_args = [
"-v", f"{self._volume_name}:{cwd}",
"-v", f"{self._volume_name}-home:/root",
"-v", f"{self._workspace_dir}:/workspace",
"-v", f"{self._home_dir}:/root",
]
else:
writable_args = [
"--tmpfs", f"{cwd}:rw,exec,size=10g",
"--tmpfs", "/workspace:rw,exec,size=10g",
"--tmpfs", "/home:rw,exec,size=1g",
"--tmpfs", "/root:rw,exec,size=1g",
]
@ -111,6 +114,11 @@ class DockerEnvironment(BaseEnvironment):
work_dir = cwd or self.cwd
effective_timeout = timeout or self.timeout
# docker exec -w doesn't expand ~, so prepend a cd into the command
if work_dir == "~" or work_dir.startswith("~/"):
exec_command = f"cd {work_dir} && {exec_command}"
work_dir = "/"
assert self._inner.container_id, "Container not started"
cmd = [self._inner.config.executable, "exec"]
if stdin_data is not None:
@ -173,16 +181,11 @@ class DockerEnvironment(BaseEnvironment):
return {"output": f"Docker execution error: {e}", "returncode": 1}
def cleanup(self):
"""Stop and remove the container. Volumes persist if persistent=True."""
"""Stop and remove the container. Bind-mount dirs persist if persistent=True."""
self._inner.cleanup()
# If NOT persistent, remove the workspace volumes too
if not self._persistent and self._volume_name:
for vol in [self._volume_name, f"{self._volume_name}-home"]:
try:
subprocess.run(
["docker", "volume", "rm", "-f", vol],
capture_output=True, timeout=10,
)
except Exception:
pass
if not self._persistent:
import shutil
for d in (self._workspace_dir, self._home_dir):
if d:
shutil.rmtree(d, ignore_errors=True)