mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-01 00:11:39 +08:00
Compare commits
2 Commits
fix/plugin
...
hermes/her
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90cf074684 | ||
|
|
436649f51c |
@@ -179,6 +179,12 @@ _MAX_COMPLETION_KEYS = (
|
|||||||
|
|
||||||
# Local server hostnames / address patterns
|
# Local server hostnames / address patterns
|
||||||
_LOCAL_HOSTS = ("localhost", "127.0.0.1", "::1", "0.0.0.0")
|
_LOCAL_HOSTS = ("localhost", "127.0.0.1", "::1", "0.0.0.0")
|
||||||
|
# Docker / Podman / Lima DNS names that resolve to the host machine
|
||||||
|
_CONTAINER_LOCAL_SUFFIXES = (
|
||||||
|
".docker.internal",
|
||||||
|
".containers.internal",
|
||||||
|
".lima.internal",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _normalize_base_url(base_url: str) -> str:
|
def _normalize_base_url(base_url: str) -> str:
|
||||||
@@ -254,6 +260,9 @@ def is_local_endpoint(base_url: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
if host in _LOCAL_HOSTS:
|
if host in _LOCAL_HOSTS:
|
||||||
return True
|
return True
|
||||||
|
# Docker / Podman / Lima internal DNS names (e.g. host.docker.internal)
|
||||||
|
if any(host.endswith(suffix) for suffix in _CONTAINER_LOCAL_SUFFIXES):
|
||||||
|
return True
|
||||||
# RFC-1918 private ranges and link-local
|
# RFC-1918 private ranges and link-local
|
||||||
import ipaddress
|
import ipaddress
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ class TestLocalStreamReadTimeout:
|
|||||||
"http://0.0.0.0:5000",
|
"http://0.0.0.0:5000",
|
||||||
"http://192.168.1.100:8000",
|
"http://192.168.1.100:8000",
|
||||||
"http://10.0.0.5:1234",
|
"http://10.0.0.5:1234",
|
||||||
|
"http://host.docker.internal:11434",
|
||||||
|
"http://host.containers.internal:11434",
|
||||||
|
"http://host.lima.internal:11434",
|
||||||
])
|
])
|
||||||
def test_local_endpoint_bumps_read_timeout(self, base_url):
|
def test_local_endpoint_bumps_read_timeout(self, base_url):
|
||||||
"""Local endpoint + default timeout -> bumps to base_timeout."""
|
"""Local endpoint + default timeout -> bumps to base_timeout."""
|
||||||
@@ -68,3 +71,38 @@ class TestLocalStreamReadTimeout:
|
|||||||
if _stream_read_timeout == 120.0 and base_url and is_local_endpoint(base_url):
|
if _stream_read_timeout == 120.0 and base_url and is_local_endpoint(base_url):
|
||||||
_stream_read_timeout = _base_timeout
|
_stream_read_timeout = _base_timeout
|
||||||
assert _stream_read_timeout == 120.0
|
assert _stream_read_timeout == 120.0
|
||||||
|
|
||||||
|
|
||||||
|
class TestIsLocalEndpoint:
|
||||||
|
"""Direct unit tests for is_local_endpoint."""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("url", [
|
||||||
|
"http://localhost:11434",
|
||||||
|
"http://127.0.0.1:8080",
|
||||||
|
"http://0.0.0.0:5000",
|
||||||
|
"http://[::1]:11434",
|
||||||
|
"http://192.168.1.100:8000",
|
||||||
|
"http://10.0.0.5:1234",
|
||||||
|
"http://172.17.0.1:11434",
|
||||||
|
])
|
||||||
|
def test_classic_local_addresses(self, url):
|
||||||
|
assert is_local_endpoint(url) is True
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("url", [
|
||||||
|
"http://host.docker.internal:11434",
|
||||||
|
"http://host.docker.internal:8080/v1",
|
||||||
|
"http://gateway.docker.internal:11434",
|
||||||
|
"http://host.containers.internal:11434",
|
||||||
|
"http://host.lima.internal:11434",
|
||||||
|
])
|
||||||
|
def test_container_dns_names(self, url):
|
||||||
|
assert is_local_endpoint(url) is True
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("url", [
|
||||||
|
"https://api.openai.com",
|
||||||
|
"https://openrouter.ai/api",
|
||||||
|
"https://api.anthropic.com",
|
||||||
|
"https://evil.docker.internal.example.com",
|
||||||
|
])
|
||||||
|
def test_remote_endpoints(self, url):
|
||||||
|
assert is_local_endpoint(url) is False
|
||||||
|
|||||||
@@ -124,6 +124,34 @@ class TestWriteToSandbox:
|
|||||||
cmd = env.execute.call_args[0][0]
|
cmd = env.execute.call_args[0][0]
|
||||||
assert "mkdir -p /data/data/com.termux/files/usr/tmp/hermes-results" in cmd
|
assert "mkdir -p /data/data/com.termux/files/usr/tmp/hermes-results" in cmd
|
||||||
|
|
||||||
|
def test_path_with_spaces_is_quoted(self):
|
||||||
|
env = MagicMock()
|
||||||
|
env.execute.return_value = {"output": "", "returncode": 0}
|
||||||
|
remote_path = "/tmp/hermes results/abc file.txt"
|
||||||
|
_write_to_sandbox("content", remote_path, env)
|
||||||
|
cmd = env.execute.call_args[0][0]
|
||||||
|
assert "'/tmp/hermes results'" in cmd
|
||||||
|
assert "'/tmp/hermes results/abc file.txt'" in cmd
|
||||||
|
|
||||||
|
def test_shell_metacharacters_neutralized(self):
|
||||||
|
"""Paths with shell metacharacters must be quoted to prevent injection."""
|
||||||
|
env = MagicMock()
|
||||||
|
env.execute.return_value = {"output": "", "returncode": 0}
|
||||||
|
malicious_path = "/tmp/hermes-results/$(whoami).txt"
|
||||||
|
_write_to_sandbox("content", malicious_path, env)
|
||||||
|
cmd = env.execute.call_args[0][0]
|
||||||
|
# The $() must not appear unquoted — shlex.quote wraps it
|
||||||
|
assert "'/tmp/hermes-results/$(whoami).txt'" in cmd
|
||||||
|
|
||||||
|
def test_semicolon_injection_neutralized(self):
|
||||||
|
env = MagicMock()
|
||||||
|
env.execute.return_value = {"output": "", "returncode": 0}
|
||||||
|
malicious_path = "/tmp/x; rm -rf /; echo .txt"
|
||||||
|
_write_to_sandbox("content", malicious_path, env)
|
||||||
|
cmd = env.execute.call_args[0][0]
|
||||||
|
# The semicolons must be inside quotes, not acting as command separators
|
||||||
|
assert "'/tmp/x; rm -rf /; echo .txt'" in cmd
|
||||||
|
|
||||||
|
|
||||||
class TestResolveStorageDir:
|
class TestResolveStorageDir:
|
||||||
def test_defaults_to_storage_dir_without_env(self):
|
def test_defaults_to_storage_dir_without_env(self):
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ Defense against context-window overflow operates at three levels:
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import shlex
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from tools.budget_config import (
|
from tools.budget_config import (
|
||||||
@@ -79,7 +80,7 @@ def _write_to_sandbox(content: str, remote_path: str, env) -> bool:
|
|||||||
marker = _heredoc_marker(content)
|
marker = _heredoc_marker(content)
|
||||||
storage_dir = os.path.dirname(remote_path)
|
storage_dir = os.path.dirname(remote_path)
|
||||||
cmd = (
|
cmd = (
|
||||||
f"mkdir -p {storage_dir} && cat > {remote_path} << '{marker}'\n"
|
f"mkdir -p {shlex.quote(storage_dir)} && cat > {shlex.quote(remote_path)} << '{marker}'\n"
|
||||||
f"{content}\n"
|
f"{content}\n"
|
||||||
f"{marker}"
|
f"{marker}"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user