NousResearch/hermes-agent

is_local_endpoint detection of Docker/Podman and local hostnames

Cluster 7905 · agent. Data as of

2 open items. Fix merged in #9248 on 5 Jun 2026. Check it covers your case.

The short answer

1open issue
1open PR
7items grouped
Known?
Yes. 7 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #9248. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #9248. That is separate from the fix event.
Still open
1 issue and 1 PR.

Members concern the agent's is_local_endpoint check not recognizing DNS-based local endpoints, such as Docker/Podman DNS names, mDNS/.lan names, and unqualified hostnames, with a related stale timeout affecting local LLM proxies. Several items are fixes proposed for this theme, and one is a bug report saying a recent fix did not cover DNS for local providers.

Open issues1

  • Issue#20346[Bug]: using DNS for local provider is not addressed in recent fix

Open PRs1

  • PR#20359fix(agent): treat mDNS/.lan/unqualified hostnames as local endpoints (#20346)

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #7950 fix: is_local_endpoint misses Docker/Podman DNS names

    merged to main

    files: agent/model_metadata.py, tests/agent/test_local_stream_timeout.py, tests/tools/test_tool_result_storage.py, tools/tool_result_storage.py

    Show the diff
    • agent/model_metadata.py
      @@ -179,6 +179,12 @@ def _strip_provider_prefix(model: str) -> str:
       
       # Local server hostnames / address patterns
       _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:
      @@ -254,6 +260,9 @@ def is_local_endpoint(base_url: str) -> bool:
               return False
           if host in _LOCAL_HOSTS:
               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
           import ipaddress
           try:
    • tests/agent/test_local_stream_timeout.py
      @@ -22,6 +22,9 @@ class TestLocalStreamReadTimeout:
               "http://0.0.0.0:5000",
               "http://192.168.1.100:8000",
               "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):
               """Local endpoint + default timeout -> bumps to base_timeout."""
      @@ -68,3 +71,38 @@ def test_empty_base_url_keeps_default(self):
                   if _stream_read_timeout == 120.0 and base_url and is_local_endpoint(base_url):
                       _stream_read_timeout = _base_timeout
                   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
    • tests/tools/test_tool_result_storage.py
      @@ -124,6 +124,34 @@ def test_uses_parent_dir_of_remote_path(self):
               cmd = env.execute.call_args[0][0]
               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:
           def test_defaults_to_storage_dir_without_env(self):
    • tools/tool_result_storage.py
      @@ -24,6 +24,7 @@
       
       import logging
       import os
      +import shlex
       import uuid
       
       from tools.budget_config import (
      @@ -79,7 +80,7 @@ def _write_to_sandbox(content: str, remote_path: str, env) -> bool:
           marker = _heredoc_marker(content)
           storage_dir = os.path.dirname(remote_path)
           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"{marker}"
           )
  • #9248 fix(local): recognize unqualified hostnames as local endpoints

    merged to main

    files: agent/model_metadata.py, tests/agent/test_local_stream_timeout.py

2 PRs closed without merging

From the close comments: superseded by another PR 1 · other 1

  • #7906 fix: is_local_endpoint misses Docker/Podman DNS names · superseded by another PR → #7950, #9248
  • #8021 fix(agent): detect Docker/Podman DNS names in is_local_endpoint · other → #7906
Closed issues · 1
  • Closed#7905is_local_endpoint misses Docker/Podman DNS names — stale timeout fires on local LLM proxies
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.26: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#7950, #9248
Closed issues
1
Attempts
2 PRs closed without merging
Fix link checked
5 Jun 2026

For your agent

The same facts, typed.