mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 01:21:43 +00:00
feat: devex help, add Makefile, ruff, pre-commit, and modernize CI
This commit is contained in:
parent
172a38c344
commit
f4d7e6a29e
111 changed files with 11655 additions and 10200 deletions
|
|
@ -11,7 +11,6 @@ import subprocess
|
|||
import sys
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from tools.environments.base import BaseEnvironment
|
||||
from tools.interrupt import is_interrupted
|
||||
|
|
@ -19,7 +18,6 @@ from tools.interrupt import is_interrupted
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
# Security flags applied to every container.
|
||||
# The container itself is the security boundary (isolated from host).
|
||||
# We drop all capabilities then add back the minimum needed:
|
||||
|
|
@ -28,19 +26,28 @@ logger = logging.getLogger(__name__)
|
|||
# Block privilege escalation and limit PIDs.
|
||||
# /tmp is size-limited and nosuid but allows exec (needed by pip/npm builds).
|
||||
_SECURITY_ARGS = [
|
||||
"--cap-drop", "ALL",
|
||||
"--cap-add", "DAC_OVERRIDE",
|
||||
"--cap-add", "CHOWN",
|
||||
"--cap-add", "FOWNER",
|
||||
"--security-opt", "no-new-privileges",
|
||||
"--pids-limit", "256",
|
||||
"--tmpfs", "/tmp:rw,nosuid,size=512m",
|
||||
"--tmpfs", "/var/tmp:rw,noexec,nosuid,size=256m",
|
||||
"--tmpfs", "/run:rw,noexec,nosuid,size=64m",
|
||||
"--cap-drop",
|
||||
"ALL",
|
||||
"--cap-add",
|
||||
"DAC_OVERRIDE",
|
||||
"--cap-add",
|
||||
"CHOWN",
|
||||
"--cap-add",
|
||||
"FOWNER",
|
||||
"--security-opt",
|
||||
"no-new-privileges",
|
||||
"--pids-limit",
|
||||
"256",
|
||||
"--tmpfs",
|
||||
"/tmp:rw,nosuid,size=512m",
|
||||
"--tmpfs",
|
||||
"/var/tmp:rw,noexec,nosuid,size=256m",
|
||||
"--tmpfs",
|
||||
"/run:rw,noexec,nosuid,size=64m",
|
||||
]
|
||||
|
||||
|
||||
_storage_opt_ok: Optional[bool] = None # cached result across instances
|
||||
_storage_opt_ok: bool | None = None # cached result across instances
|
||||
|
||||
|
||||
class DockerEnvironment(BaseEnvironment):
|
||||
|
|
@ -74,7 +81,7 @@ class DockerEnvironment(BaseEnvironment):
|
|||
self._base_image = image
|
||||
self._persistent = persistent_filesystem
|
||||
self._task_id = task_id
|
||||
self._container_id: Optional[str] = None
|
||||
self._container_id: str | None = None
|
||||
logger.info(f"DockerEnvironment volumes: {volumes}")
|
||||
# Ensure volumes is a list (config.yaml could be malformed)
|
||||
if volumes is not None and not isinstance(volumes, list):
|
||||
|
|
@ -105,8 +112,8 @@ class DockerEnvironment(BaseEnvironment):
|
|||
# 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
|
||||
self._workspace_dir: str | None = None
|
||||
self._home_dir: str | None = None
|
||||
if self._persistent:
|
||||
sandbox = get_sandbox_dir() / "docker" / task_id
|
||||
self._workspace_dir = str(sandbox / "workspace")
|
||||
|
|
@ -114,14 +121,19 @@ class DockerEnvironment(BaseEnvironment):
|
|||
os.makedirs(self._workspace_dir, exist_ok=True)
|
||||
os.makedirs(self._home_dir, exist_ok=True)
|
||||
writable_args = [
|
||||
"-v", f"{self._workspace_dir}:/workspace",
|
||||
"-v", f"{self._home_dir}:/root",
|
||||
"-v",
|
||||
f"{self._workspace_dir}:/workspace",
|
||||
"-v",
|
||||
f"{self._home_dir}:/root",
|
||||
]
|
||||
else:
|
||||
writable_args = [
|
||||
"--tmpfs", "/workspace:rw,exec,size=10g",
|
||||
"--tmpfs", "/home:rw,exec,size=1g",
|
||||
"--tmpfs", "/root:rw,exec,size=1g",
|
||||
"--tmpfs",
|
||||
"/workspace:rw,exec,size=10g",
|
||||
"--tmpfs",
|
||||
"/home:rw,exec,size=1g",
|
||||
"--tmpfs",
|
||||
"/root:rw,exec,size=1g",
|
||||
]
|
||||
|
||||
# All containers get security hardening (capabilities dropped, no privilege
|
||||
|
|
@ -129,7 +141,7 @@ class DockerEnvironment(BaseEnvironment):
|
|||
# can install packages as needed.
|
||||
# User-configured volume mounts (from config.yaml docker_volumes)
|
||||
volume_args = []
|
||||
for vol in (volumes or []):
|
||||
for vol in volumes or []:
|
||||
if not isinstance(vol, str):
|
||||
logger.warning(f"Docker volume entry is not a string: {vol!r}")
|
||||
continue
|
||||
|
|
@ -146,7 +158,9 @@ class DockerEnvironment(BaseEnvironment):
|
|||
logger.info(f"Docker run_args: {all_run_args}")
|
||||
|
||||
self._inner = _Docker(
|
||||
image=image, cwd=cwd, timeout=timeout,
|
||||
image=image,
|
||||
cwd=cwd,
|
||||
timeout=timeout,
|
||||
run_args=all_run_args,
|
||||
)
|
||||
self._container_id = self._inner.container_id
|
||||
|
|
@ -154,7 +168,7 @@ class DockerEnvironment(BaseEnvironment):
|
|||
@staticmethod
|
||||
def _storage_opt_supported() -> bool:
|
||||
"""Check if Docker's storage driver supports --storage-opt size=.
|
||||
|
||||
|
||||
Only overlay2 on XFS with pquota supports per-container disk quotas.
|
||||
Ubuntu (and most distros) default to ext4, where this flag errors out.
|
||||
"""
|
||||
|
|
@ -164,7 +178,9 @@ class DockerEnvironment(BaseEnvironment):
|
|||
try:
|
||||
result = subprocess.run(
|
||||
["docker", "info", "--format", "{{.Driver}}"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
driver = result.stdout.strip().lower()
|
||||
if driver != "overlay2":
|
||||
|
|
@ -174,14 +190,15 @@ class DockerEnvironment(BaseEnvironment):
|
|||
# Probe by attempting a dry-ish run — the fastest reliable check.
|
||||
probe = subprocess.run(
|
||||
["docker", "create", "--storage-opt", "size=1m", "hello-world"],
|
||||
capture_output=True, text=True, timeout=15,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
)
|
||||
if probe.returncode == 0:
|
||||
# Clean up the created container
|
||||
container_id = probe.stdout.strip()
|
||||
if container_id:
|
||||
subprocess.run(["docker", "rm", container_id],
|
||||
capture_output=True, timeout=5)
|
||||
subprocess.run(["docker", "rm", container_id], capture_output=True, timeout=5)
|
||||
_storage_opt_ok = True
|
||||
else:
|
||||
_storage_opt_ok = False
|
||||
|
|
@ -190,9 +207,9 @@ class DockerEnvironment(BaseEnvironment):
|
|||
logger.debug("Docker --storage-opt support: %s", _storage_opt_ok)
|
||||
return _storage_opt_ok
|
||||
|
||||
def execute(self, command: str, cwd: str = "", *,
|
||||
timeout: int | None = None,
|
||||
stdin_data: str | None = None) -> dict:
|
||||
def execute(
|
||||
self, command: str, cwd: str = "", *, timeout: int | None = None, stdin_data: str | None = None
|
||||
) -> dict:
|
||||
exec_command = self._prepare_command(command)
|
||||
work_dir = cwd or self.cwd
|
||||
effective_timeout = timeout or self.timeout
|
||||
|
|
@ -218,7 +235,8 @@ class DockerEnvironment(BaseEnvironment):
|
|||
_output_chunks = []
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
stdin=subprocess.PIPE if stdin_data else subprocess.DEVNULL,
|
||||
text=True,
|
||||
)
|
||||
|
|
@ -269,6 +287,7 @@ class DockerEnvironment(BaseEnvironment):
|
|||
|
||||
if not self._persistent:
|
||||
import shutil
|
||||
|
||||
for d in (self._workspace_dir, self._home_dir):
|
||||
if d:
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue