From 931232e41882d263b8a559bcec1a7e239a85b690 Mon Sep 17 00:00:00 2001 From: Edwin He Date: Thu, 16 Jul 2026 19:30:42 +0000 Subject: [PATCH 1/2] Fix validate_tool hang: pass stdin=DEVNULL to the validation subprocess MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `validate_tool` runs each tool's smoke test (e.g. `pi --print "say hi in 5 words or less"`) via `subprocess.run(..., timeout=60)` without setting `stdin`, so the child inherits ucode's own stdin. In print/headless mode some agent CLIs (pi among them) still open stdin and block waiting for input/EOF. When ucode is launched from a non-interactive parent whose stdin is an open pipe with no EOF — e.g. an agent harness spawning `ucode ` as a subprocess — the validation command never sees EOF, blocks for the full 60s, and `validate_tool` returns `(False, "timed out")`. The caller (`_auto_configure_tool` / configure) then reverts the config and aborts with " validation failed — config reverted", even though the tool is correctly configured and the gateway is reachable. The validation smoke test is one-shot and never needs interactive input, so pass `stdin=subprocess.DEVNULL`. This is benign for every tool (an interactive prompt in a validation run would itself be a bug) and makes validation robust regardless of how ucode was launched. Verified on a managed sandbox: `pi --print "say hi"` returns instantly with `stdin=DEVNULL` but hangs to the timeout when it inherits an open stdin pipe. Signed-off-by: Edwin He --- src/ucode/agents/__init__.py | 16 +++++++++++++++- tests/test_agents_init.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/ucode/agents/__init__.py b/src/ucode/agents/__init__.py index f44b91b..25e86b4 100644 --- a/src/ucode/agents/__init__.py +++ b/src/ucode/agents/__init__.py @@ -465,8 +465,22 @@ def validate_tool(tool: str) -> tuple[bool, str]: except RuntimeError: env = None try: + # stdin=DEVNULL: the validate command is a one-shot smoke test (e.g. + # `pi --print "say hi..."`) that never needs interactive input. Some + # agent CLIs still open stdin in print mode and block on it; without + # this, `subprocess.run` inherits our stdin, so when ucode itself is + # launched from a non-interactive parent whose stdin is an open pipe + # with no EOF (e.g. an agent harness spawning `ucode `), the + # validation subprocess hangs until the 60s timeout and reports a + # spurious "timed out" / "validation failed". result = subprocess.run( - cmd, check=False, capture_output=True, text=True, timeout=60, env=env + cmd, + check=False, + capture_output=True, + text=True, + timeout=60, + env=env, + stdin=subprocess.DEVNULL, ) if result.returncode == 0: return True, "" diff --git a/tests/test_agents_init.py b/tests/test_agents_init.py index 576fc22..1a90ff8 100644 --- a/tests/test_agents_init.py +++ b/tests/test_agents_init.py @@ -567,3 +567,37 @@ def test_low_verbosity_omits_panels(self, monkeypatch, capsys): assert "Ready" not in out # Per-tool success line is still printed. assert "Codex is working" in out + + +class TestValidateTool: + def test_runs_validate_command_with_stdin_devnull(self, monkeypatch): + # Regression guard: the validation smoke test must never inherit the + # caller's stdin, or it hangs to the timeout when ucode is launched + # from a non-interactive parent whose stdin is an open pipe. + captured: dict = {} + + def fake_run(cmd, **kwargs): + captured["cmd"] = cmd + captured["kwargs"] = kwargs + return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="") + + monkeypatch.setattr("ucode.agents.subprocess.run", fake_run) + monkeypatch.setattr(agents_mod, "load_state", lambda: {}) + + ok, err = agents_mod.validate_tool("codex") + + assert ok is True + assert err == "" + assert captured["kwargs"].get("stdin") is subprocess.DEVNULL + + def test_reports_timed_out_on_timeout(self, monkeypatch): + def fake_run(cmd, **kwargs): + raise subprocess.TimeoutExpired(cmd, 60) + + monkeypatch.setattr("ucode.agents.subprocess.run", fake_run) + monkeypatch.setattr(agents_mod, "load_state", lambda: {}) + + ok, err = agents_mod.validate_tool("codex") + + assert ok is False + assert err == "timed out" From 03f3a9b72950c27b6bab6988f81514a4e999ef3d Mon Sep 17 00:00:00 2001 From: Edwin He Date: Fri, 17 Jul 2026 18:33:54 +0000 Subject: [PATCH 2/2] Remove explanatory comment on the validate_tool stdin=DEVNULL kwarg Per review: the kwarg is self-explanatory; drop the comment block. --- src/ucode/agents/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/ucode/agents/__init__.py b/src/ucode/agents/__init__.py index 25e86b4..127cc7a 100644 --- a/src/ucode/agents/__init__.py +++ b/src/ucode/agents/__init__.py @@ -465,14 +465,6 @@ def validate_tool(tool: str) -> tuple[bool, str]: except RuntimeError: env = None try: - # stdin=DEVNULL: the validate command is a one-shot smoke test (e.g. - # `pi --print "say hi..."`) that never needs interactive input. Some - # agent CLIs still open stdin in print mode and block on it; without - # this, `subprocess.run` inherits our stdin, so when ucode itself is - # launched from a non-interactive parent whose stdin is an open pipe - # with no EOF (e.g. an agent harness spawning `ucode `), the - # validation subprocess hangs until the 60s timeout and reports a - # spurious "timed out" / "validation failed". result = subprocess.run( cmd, check=False,