drydock-cli 3.0.88__tar.gz → 3.0.90__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {drydock_cli-3.0.88/drydock_cli.egg-info → drydock_cli-3.0.90}/PKG-INFO +1 -1
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/tools/__init__.py +27 -3
- {drydock_cli-3.0.88 → drydock_cli-3.0.90/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock_cli.egg-info/SOURCES.txt +2 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/pyproject.toml +1 -1
- drydock_cli-3.0.90/tests/test_bash_background.py +44 -0
- drydock_cli-3.0.90/tests/test_bash_stdin.py +40 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/LICENSE +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/NOTICE +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/README.md +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/agent.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/cci.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/cli.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/config.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/detect.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/extract.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/guards.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/poam.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/providers.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/skills.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/stig.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock/web.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/setup.cfg +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_config.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.88 → drydock_cli-3.0.90}/tests/test_xccdf.py +0 -0
|
@@ -911,6 +911,13 @@ def tool_bash(params: dict, config: dict) -> str:
|
|
|
911
911
|
try:
|
|
912
912
|
proc = subprocess.Popen(
|
|
913
913
|
cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
914
|
+
# stdin=DEVNULL so a command that reads stdin gets immediate EOF
|
|
915
|
+
# (correct for a non-interactive tool) instead of inheriting the
|
|
916
|
+
# TUI's terminal — where it would steal the user's keystrokes or hang
|
|
917
|
+
# waiting for input Textual has captured. The agent still feeds input
|
|
918
|
+
# explicitly via a pipe/redirect (echo x | cmd, cmd < file), which
|
|
919
|
+
# overrides this.
|
|
920
|
+
stdin=subprocess.DEVNULL,
|
|
914
921
|
text=True, cwd=config.get("cwd"), start_new_session=True,
|
|
915
922
|
)
|
|
916
923
|
config.setdefault("_abort", {})["proc"] = proc
|
|
@@ -936,6 +943,7 @@ def tool_bash(params: dict, config: dict) -> str:
|
|
|
936
943
|
reader = threading.Thread(target=_drain, daemon=True)
|
|
937
944
|
reader.start()
|
|
938
945
|
start = time.monotonic()
|
|
946
|
+
backgrounded = False
|
|
939
947
|
while reader.is_alive():
|
|
940
948
|
reader.join(0.3)
|
|
941
949
|
if capped.is_set():
|
|
@@ -945,6 +953,17 @@ def tool_bash(params: dict, config: dict) -> str:
|
|
|
945
953
|
kill_process_group(proc)
|
|
946
954
|
proc.wait()
|
|
947
955
|
return "[stopped by user]"
|
|
956
|
+
# The SHELL has exited but the pipe is still open → the command
|
|
957
|
+
# backgrounded a child (`cmd &`, a server the task wants to keep
|
|
958
|
+
# running) that inherited stdout. Don't wait for it (that would hang
|
|
959
|
+
# until the timeout) and DON'T kill it — return what we have so the
|
|
960
|
+
# background process survives. (Redirecting its output, `cmd >log &`,
|
|
961
|
+
# closes the pipe and never reaches here.)
|
|
962
|
+
if proc.poll() is not None:
|
|
963
|
+
reader.join(0.5) # brief grace for any final buffered output
|
|
964
|
+
if reader.is_alive():
|
|
965
|
+
backgrounded = True
|
|
966
|
+
break
|
|
948
967
|
if time.monotonic() - start > timeout:
|
|
949
968
|
kill_process_group(proc)
|
|
950
969
|
proc.wait()
|
|
@@ -958,11 +977,16 @@ def tool_bash(params: dict, config: dict) -> str:
|
|
|
958
977
|
if _is_network_command(cmd):
|
|
959
978
|
msg += _OFFLINE_HINT
|
|
960
979
|
return msg
|
|
961
|
-
|
|
980
|
+
if not backgrounded:
|
|
981
|
+
proc.wait()
|
|
962
982
|
# Collapse repetitive runs FIRST (turns 256 KB of "y\n" into ~2 lines),
|
|
963
983
|
# then note if we hit the byte cap. Bounds both RAM (the cap) and context
|
|
964
|
-
# tokens (the collapse).
|
|
965
|
-
|
|
984
|
+
# tokens (the collapse). Snapshot chunks (list()) in case the reader
|
|
985
|
+
# daemon is still appending for a backgrounded child.
|
|
986
|
+
output = _collapse_repeated_lines("".join(list(chunks)))
|
|
987
|
+
if backgrounded:
|
|
988
|
+
return (output.rstrip() + "\n[a process was left running in the background; "
|
|
989
|
+
"the command returned. Check it with a follow-up command.]").lstrip("\n")
|
|
966
990
|
if capped.is_set():
|
|
967
991
|
return (
|
|
968
992
|
output.rstrip()
|
|
@@ -49,9 +49,11 @@ drydock_cli.egg-info/top_level.txt
|
|
|
49
49
|
tests/test_advisor.py
|
|
50
50
|
tests/test_approval.py
|
|
51
51
|
tests/test_back_command.py
|
|
52
|
+
tests/test_bash_background.py
|
|
52
53
|
tests/test_bash_output_bounding.py
|
|
53
54
|
tests/test_bash_process_group.py
|
|
54
55
|
tests/test_bash_safety.py
|
|
56
|
+
tests/test_bash_stdin.py
|
|
55
57
|
tests/test_bash_timeout_network.py
|
|
56
58
|
tests/test_cci.py
|
|
57
59
|
tests/test_cli_agents.py
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
# PyPI distribution name is drydock-cli (the established install name, continued
|
|
8
8
|
# from the retired v2 fork); the import package + CLI command stay `drydock`.
|
|
9
9
|
name = "drydock-cli"
|
|
10
|
-
version = "3.0.
|
|
10
|
+
version = "3.0.90"
|
|
11
11
|
description = "Drydock — a local, provider-agnostic terminal coding agent for local LLMs"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.11"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""tool_bash must support backgrounded processes (`cmd &`) — a server/daemon the
|
|
2
|
+
task wants to keep running — instead of hanging to the timeout then killing it.
|
|
3
|
+
Found via the kv-store-grpc tbench task (needs a background gRPC server)."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from drydock.tools import tool_bash
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _timed(cmd, timeout=8):
|
|
13
|
+
t0 = time.monotonic()
|
|
14
|
+
out = tool_bash({"command": cmd, "timeout": timeout}, {"cwd": "/tmp"})
|
|
15
|
+
return time.monotonic() - t0, out
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_backgrounded_command_returns_fast_and_survives():
|
|
19
|
+
try:
|
|
20
|
+
dt, out = _timed("sleep 12 & echo bg-started")
|
|
21
|
+
assert dt < 3.0, f"backgrounded cmd hung ({dt:.1f}s) — should return promptly"
|
|
22
|
+
assert "background" in out.lower()
|
|
23
|
+
time.sleep(0.5)
|
|
24
|
+
n = subprocess.run("pgrep -f 'sleep 12' | wc -l", shell=True,
|
|
25
|
+
capture_output=True, text=True).stdout.strip()
|
|
26
|
+
assert int(n) >= 1, "backgrounded process was killed (should survive)"
|
|
27
|
+
finally:
|
|
28
|
+
subprocess.run("pkill -f 'sleep 12'", shell=True)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_normal_command_unaffected():
|
|
32
|
+
dt, out = _timed("echo hello && echo world")
|
|
33
|
+
assert "hello" in out and "world" in out and dt < 2.0
|
|
34
|
+
assert "background" not in out.lower()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_foreground_hang_still_times_out():
|
|
38
|
+
dt, out = _timed("sleep 30", timeout=3)
|
|
39
|
+
assert "timed out" in out and dt < 6.0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_failing_command_still_reports_exit_code():
|
|
43
|
+
_, out = _timed("false")
|
|
44
|
+
assert "[exit code: 1]" in out
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""tool_bash must NOT inherit the caller's stdin (in the TUI that's Textual's
|
|
2
|
+
terminal): a command that reads stdin should get immediate EOF, not hang waiting
|
|
3
|
+
for input or steal the user's keystrokes. The agent still feeds input explicitly
|
|
4
|
+
via a pipe/redirect, which overrides the DEVNULL stdin."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from drydock.tools import tool_bash
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _run(cmd, timeout=6):
|
|
13
|
+
t0 = time.monotonic()
|
|
14
|
+
out = tool_bash({"command": cmd, "timeout": timeout}, {"cwd": "/tmp"})
|
|
15
|
+
return time.monotonic() - t0, out
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_stdin_reader_gets_eof_not_hang():
|
|
19
|
+
dt, out = _run("read x; echo \"got:[$x]\"")
|
|
20
|
+
assert dt < 2.0 and "got:[]" in out, "reading stdin should EOF instantly, not hang"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_cat_no_input_eofs():
|
|
24
|
+
dt, out = _run("cat && echo DONE")
|
|
25
|
+
assert dt < 2.0 and "DONE" in out
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_python_input_raises_eoferror_cleanly():
|
|
29
|
+
_, out = _run('python3 -c "input()" 2>&1 | tail -1')
|
|
30
|
+
assert "EOFError" in out
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_piped_input_still_works():
|
|
34
|
+
_, out = _run("echo hello | cat")
|
|
35
|
+
assert "hello" in out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_redirect_from_file_still_works():
|
|
39
|
+
_, out = _run("printf 'a\\nb\\n' > /tmp/ddin.txt; wc -l < /tmp/ddin.txt")
|
|
40
|
+
assert "2" in out
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|