OpenHands / OpenHands/software-agent-sdk

Support Unix domain socket binding in openhands-agent-server

Open
#4,109 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
Python
Stars
1.1k
Forks
542
Avg merge
1d 19h
Merged PRs (30d)
137

Description

Problem

openhands-agent-server currently exposes only TCP binding through --host and --port. There is no supported way to bind the server to a Unix domain socket.

This matters for local container deployments where each isolated task has its own Agent Server. A Unix socket can provide filesystem-scoped connectivity without publishing a per-task TCP port or introducing another container network/proxy.

Reproduction

Tested with OpenHands SDK 1.35.0, using the binary shipped in Agent Canvas 1.3.0:

$ openhands-agent-server --help
usage: openhands-agent-server [-h] [--host HOST] [--port PORT] [--reload]
                              [--check-browser]
                              [--import-modules IMPORT_MODULES]
                              [--extra-python-path EXTRA_PYTHON_PATH]

$ openhands-agent-server --uds /tmp/agent-server.sock
openhands-agent-server: error: unrecognized arguments: --uds /tmp/agent-server.sock
$ echo $?
2

I searched existing open and closed issues for "unix socket", "unix domain socket", and "UDS" and did not find a duplicate.

Requested contract

  • Add --uds PATH, mutually exclusive with explicitly supplied --host / --port.
  • Preserve HTTP and WebSocket behavior.
  • Make relative LookupSecret URLs use the same UDS transport; simply passing uds= to Uvicorn is insufficient because OH_INTERNAL_SERVER_URL currently points internal secret lookups back to TCP.
  • Fail clearly for unsupported platforms or unsafe/unusable paths.
  • Document socket ownership, permissions, stale-socket cleanup, and reload behavior.
  • Add CLI/config and relative-secret integration coverage.

If there is already a supported public ASGI entry point that can be run under Uvicorn with --uds without depending on private module internals, documenting it would also solve the immediate problem.

Proposed patch

The following is a concrete minimal direction against current main (current files: openhands-agent-server/openhands/agent_server/__main__.py, openhands-sdk/openhands/sdk/secret/secrets.py, and tests/agent_server/test_preload_modules.py):

diff --git a/openhands-agent-server/openhands/agent_server/__main__.py b/openhands-agent-server/openhands/agent_server/__main__.py
--- a/openhands-agent-server/openhands/agent_server/__main__.py
+++ b/openhands-agent-server/openhands/agent_server/__main__.py
@@
 _INTERNAL_SERVER_URL_ENV = "OH_INTERNAL_SERVER_URL"
+_INTERNAL_SERVER_UDS_ENV = "OH_INTERNAL_SERVER_UDS"
 _EXTRA_PYTHON_PATH_ENV = "OH_EXTRA_PYTHON_PATH"
@@
     parser.add_argument(
         "--port", type=int, default=8000, help="Port to bind to (default: 8000)"
     )
+    parser.add_argument(
+        "--uds",
+        default=None,
+        help="Unix domain socket to bind instead of TCP",
+    )
@@
     args = parser.parse_args()
+    if args.uds and any(flag in sys.argv[1:] for flag in ("--host", "--port")):
+        parser.error("--uds cannot be combined with --host or --port")
+    if args.uds and os.name == "nt":
+        parser.error("--uds is not supported on Windows")
@@
-    os.environ[_INTERNAL_SERVER_URL_ENV] = _get_internal_server_url(
-        args.host, args.port
-    )
+    if args.uds:
+        os.environ[_INTERNAL_SERVER_URL_ENV] = "http://localhost"
+        os.environ[_INTERNAL_SERVER_UDS_ENV] = args.uds
+    else:
+        os.environ.pop(_INTERNAL_SERVER_UDS_ENV, None)
+        os.environ[_INTERNAL_SERVER_URL_ENV] = _get_internal_server_url(
+            args.host, args.port
+        )
@@
-    config = Config(
-        "openhands.agent_server.api:api",
-        host=args.host,
-        port=args.port,
+    config_kwargs = dict(
         reload=args.reload,
         reload_includes=[
             "openhands-agent-server",
             "openhands-sdk",
             "openhands-tools",
         ],
         log_level=log_level,
         log_config=LOGGING_CONFIG,
         ws="wsproto",
     )
+    if args.uds:
+        config = Config(
+            "openhands.agent_server.api:api", uds=args.uds, **config_kwargs
+        )
+    else:
+        config = Config(
+            "openhands.agent_server.api:api",
+            host=args.host,
+            port=args.port,
+            **config_kwargs,
+        )

diff --git a/openhands-sdk/openhands/sdk/secret/secrets.py b/openhands-sdk/openhands/sdk/secret/secrets.py
--- a/openhands-sdk/openhands/sdk/secret/secrets.py
+++ b/openhands-sdk/openhands/sdk/secret/secrets.py
@@
 _INTERNAL_SERVER_URL_ENV = "OH_INTERNAL_SERVER_URL"
+_INTERNAL_SERVER_UDS_ENV = "OH_INTERNAL_SERVER_UDS"
 _DEFAULT_INTERNAL_SERVER_URL = "http://127.0.0.1:8000"
@@
     def get_value(self) -> str:
-        response = httpx.get(self.url, headers=self.headers, timeout=30.0)
+        uds = os.getenv(_INTERNAL_SERVER_UDS_ENV)
+        if uds:
+            transport = httpx.HTTPTransport(uds=uds)
+            with httpx.Client(transport=transport, timeout=30.0) as client:
+                response = client.get(self.url, headers=self.headers)
+        else:
+            response = httpx.get(self.url, headers=self.headers, timeout=30.0)
         response.raise_for_status()
         return response.text

diff --git a/tests/agent_server/test_preload_modules.py b/tests/agent_server/test_preload_modules.py
--- a/tests/agent_server/test_preload_modules.py
+++ b/tests/agent_server/test_preload_modules.py
@@
 class TestMainCheckBrowserOrdering:
+    def test_main_configures_unix_socket(self, monkeypatch, tmp_path):
+        socket_path = tmp_path / "agent-server.sock"
+        monkeypatch.delenv("OH_INTERNAL_SERVER_UDS", raising=False)
+
+        with (
+            patch("sys.argv", ["prog", "--uds", str(socket_path)]),
+            patch("openhands.agent_server.__main__.preload_modules"),
+            patch("openhands.agent_server.__main__.LoggingServer") as server_cls,
+            patch("openhands.agent_server.__main__.Config") as config,
+        ):
+            server_cls.return_value.run.side_effect = SystemExit(0)
+            from openhands.agent_server.__main__ import main
+            with pytest.raises(SystemExit):
+                main()
+
+        assert os.environ["OH_INTERNAL_SERVER_URL"] == "http://localhost"
+        assert os.environ["OH_INTERNAL_SERVER_UDS"] == str(socket_path)
+        assert config.call_args.kwargs["uds"] == str(socket_path)
+        assert "host" not in config.call_args.kwargs
+        assert "port" not in config.call_args.kwargs

The config_kwargs helper may need a small typed-dict annotation to satisfy the repository's type checker. The important part is treating Uvicorn binding and internal LookupSecret transport as one contract. I would also add a focused LookupSecret test that asserts httpx.HTTPTransport(uds=...) is selected for a relative URL.

Context

The goal is to avoid a custom ingress/proxy when all consumers are local and filesystem permissions can define the transport boundary. This is a feature request, not a claim that the current TCP interface is broken.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at openhands-agent-server/openhands/agent_server/main.py and trace how the CLI builds its server configuration and internal URL. Then read openhands-sdk/openhands/sdk/secret/secrets.py and tests/agent_server/test_preload_modules.py, adding focused coverage for the relative-secret transport. Done means the UDS CLI/config path, secret lookups, validation, documentation, and integration tests cover the requested contract.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, backend, cli
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.