merge: sync with upstream NousResearch/hermes-agent (499 commits)
Some checks failed
Forge CI / smoke-and-build (pull_request) Failing after 0s

Resolves all 10 conflicts by keeping upstream versions of core files.
Our 142 unique additions (wizard-bootstrap, CI, docs, tests, agent evolution) preserved.

Upstream highlights:
- Browser Use replaces Browserbase
- notify_on_complete for background processes
- Permanent command allowlist for approvals
- Reasoning block display fixes
- Credential pool auto-detection
- Many bug fixes and improvements
This commit is contained in:
Alexander Whitestone
2026-04-07 10:00:16 -04:00
160 changed files with 40216 additions and 279 deletions

View File

@@ -286,6 +286,26 @@ class DockerEnvironment(BaseEnvironment):
# mode uses tmpfs (ephemeral, fast, gone on cleanup).
from tools.environments.base import get_sandbox_dir
# SECURITY FIX (V-012): Block dangerous volume mounts
# Prevent privilege escalation via Docker socket or sensitive paths
_BLOCKED_VOLUME_PATTERNS = [
"/var/run/docker.sock",
"/run/docker.sock",
"/var/run/docker.pid",
"/proc", "/sys", "/dev",
":/", # Root filesystem mount
]
def _is_dangerous_volume(vol_spec: str) -> bool:
"""Check if volume spec is dangerous (docker socket, root fs, etc)."""
for pattern in _BLOCKED_VOLUME_PATTERNS:
if pattern in vol_spec:
return True
# Check for docker socket variations
if "docker.sock" in vol_spec.lower():
return True
return False
# User-configured volume mounts (from config.yaml docker_volumes)
volume_args = []
workspace_explicitly_mounted = False
@@ -296,6 +316,15 @@ class DockerEnvironment(BaseEnvironment):
vol = vol.strip()
if not vol:
continue
# SECURITY FIX (V-012): Block dangerous volumes
if _is_dangerous_volume(vol):
logger.error(
f"SECURITY: Refusing to mount dangerous volume '{vol}'. "
f"Docker socket and system paths are blocked to prevent container escape."
)
continue # Skip this dangerous volume
if ":" in vol:
volume_args.extend(["-v", vol])
if ":/workspace" in vol:
@@ -575,22 +604,48 @@ class DockerEnvironment(BaseEnvironment):
"""Stop and remove the container. Bind-mount dirs persist if persistent=True."""
if self._container_id:
try:
# SECURITY FIX: Use list-based commands instead of shell=True
# to prevent command injection via malicious container IDs
# Stop in background so cleanup doesn't block
stop_cmd = (
f"(timeout 60 {self._docker_exe} stop {self._container_id} || "
f"{self._docker_exe} rm -f {self._container_id}) >/dev/null 2>&1 &"
container_id = self._container_id
# Validate container ID format to prevent injection
if not re.match(r'^[a-f0-9]{12,64}$', container_id):
logger.warning("Invalid container ID format: %s", container_id)
return
# Use subprocess with list args instead of shell=True
subprocess.Popen(
["timeout", "60", self._docker_exe, "stop", container_id],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
subprocess.Popen(stop_cmd, shell=True)
except Exception as e:
logger.warning("Failed to stop container %s: %s", self._container_id, e)
if not self._persistent:
# Also schedule removal (stop only leaves it as stopped)
try:
subprocess.Popen(
f"sleep 3 && {self._docker_exe} rm -f {self._container_id} >/dev/null 2>&1 &",
shell=True,
# Use a delayed removal via threading instead of shell
def delayed_remove(docker_exe, container_id, delay=3):
import time
time.sleep(delay)
try:
subprocess.run(
[docker_exe, "rm", "-f", container_id],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)
except Exception:
pass
import threading
remove_thread = threading.Thread(
target=delayed_remove,
args=(self._docker_exe, self._container_id, 3),
daemon=True,
)
remove_thread.start()
except Exception:
pass
self._container_id = None