drydock-cli 3.0.93__tar.gz → 3.0.95__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.93/drydock_cli.egg-info → drydock_cli-3.0.95}/PKG-INFO +1 -1
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/tools/__init__.py +28 -1
- {drydock_cli-3.0.93 → drydock_cli-3.0.95/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock_cli.egg-info/SOURCES.txt +1 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/pyproject.toml +1 -1
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_bash_background.py +20 -0
- drydock_cli-3.0.95/tests/test_bash_sanitize.py +35 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/LICENSE +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/NOTICE +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/README.md +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/agent.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/cci.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/cli.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/config.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/detect.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/extract.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/guards.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/poam.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/providers.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/skills.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/stig.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock/web.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/setup.cfg +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_config.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.93 → drydock_cli-3.0.95}/tests/test_xccdf.py +0 -0
|
@@ -21,6 +21,7 @@ from pathlib import Path
|
|
|
21
21
|
# to gigabytes within the timeout. We stream with a byte cap and kill the command
|
|
22
22
|
# once it's hit — bounding RAM regardless of how much it tries to produce.
|
|
23
23
|
_MAX_BASH_OUTPUT_BYTES = 256 * 1024 # 256 KB — plenty of context, safe for RAM
|
|
24
|
+
_PARTIAL_TAIL = 4000 # chars of pre-timeout output to keep (tail) in the timeout msg
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
def _detect_bash() -> str | None:
|
|
@@ -46,6 +47,20 @@ def _detect_bash() -> str | None:
|
|
|
46
47
|
_BASH_SHELL = _detect_bash()
|
|
47
48
|
|
|
48
49
|
|
|
50
|
+
# ANSI escape sequences (CSI colour/cursor, OSC title, and lone two-char escapes).
|
|
51
|
+
# Some tools emit these even to a pipe (--color=always, forced-colour test runners),
|
|
52
|
+
# and they're pure display control — noise that wastes the model's tokens.
|
|
53
|
+
_ANSI_RE = re.compile(r"\x1b(?:\[[0-9;?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-Z\\-_])")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _sanitize_bash_output(text: str) -> str:
|
|
57
|
+
"""Strip ANSI escape sequences and drop NUL bytes from command output. NUL is
|
|
58
|
+
valid UTF-8 (so the errors='replace' decode keeps it) but raw NUL in a JSON
|
|
59
|
+
tool-result trips some LLM servers' tokenizers; ANSI codes are display noise.
|
|
60
|
+
Neither carries information the model needs. Newlines/tabs/Unicode are kept."""
|
|
61
|
+
return _ANSI_RE.sub("", text).replace("\x00", "")
|
|
62
|
+
|
|
63
|
+
|
|
49
64
|
def _collapse_repeated_lines(text: str, run: int = 20) -> str:
|
|
50
65
|
"""Collapse a run of >= `run` IDENTICAL consecutive lines into one line + a
|
|
51
66
|
count. Repetitive output (`yes`, a spinning progress log) tokenizes densely —
|
|
@@ -1006,6 +1021,18 @@ def tool_bash(params: dict, config: dict) -> str:
|
|
|
1006
1021
|
)
|
|
1007
1022
|
if _is_network_command(cmd):
|
|
1008
1023
|
msg += _OFFLINE_HINT
|
|
1024
|
+
# Preserve any output produced BEFORE the hang — a test run or
|
|
1025
|
+
# build that prints results/diagnostics then stalls would
|
|
1026
|
+
# otherwise lose exactly what the agent needs. Tail-bounded so a
|
|
1027
|
+
# big partial can't bloat context.
|
|
1028
|
+
partial = _sanitize_bash_output(
|
|
1029
|
+
_collapse_repeated_lines("".join(list(chunks)))
|
|
1030
|
+
).rstrip()
|
|
1031
|
+
if partial:
|
|
1032
|
+
tail = partial[-_PARTIAL_TAIL:]
|
|
1033
|
+
if len(partial) > _PARTIAL_TAIL:
|
|
1034
|
+
tail = "[... earlier output truncated ...]\n" + tail
|
|
1035
|
+
msg += f"\n\n--- output before timeout ---\n{tail}"
|
|
1009
1036
|
return msg
|
|
1010
1037
|
if not backgrounded:
|
|
1011
1038
|
proc.wait()
|
|
@@ -1013,7 +1040,7 @@ def tool_bash(params: dict, config: dict) -> str:
|
|
|
1013
1040
|
# then note if we hit the byte cap. Bounds both RAM (the cap) and context
|
|
1014
1041
|
# tokens (the collapse). Snapshot chunks (list()) in case the reader
|
|
1015
1042
|
# daemon is still appending for a backgrounded child.
|
|
1016
|
-
output = _collapse_repeated_lines("".join(list(chunks)))
|
|
1043
|
+
output = _sanitize_bash_output(_collapse_repeated_lines("".join(list(chunks))))
|
|
1017
1044
|
if backgrounded:
|
|
1018
1045
|
return (output.rstrip() + "\n[a process was left running in the background; "
|
|
1019
1046
|
"the command returned. Check it with a follow-up command.]").lstrip("\n")
|
|
@@ -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.95"
|
|
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"
|
|
@@ -42,3 +42,23 @@ def test_foreground_hang_still_times_out():
|
|
|
42
42
|
def test_failing_command_still_reports_exit_code():
|
|
43
43
|
_, out = _timed("false")
|
|
44
44
|
assert "[exit code: 1]" in out
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_timeout_preserves_partial_output():
|
|
48
|
+
"""A command that prints useful output then hangs must return that output
|
|
49
|
+
alongside the timeout message — not discard it (test runs/builds that stall
|
|
50
|
+
after printing results/diagnostics)."""
|
|
51
|
+
from drydock.tools import tool_bash
|
|
52
|
+
out = tool_bash(
|
|
53
|
+
{"command": "echo IMPORTANT_RESULT; echo line2; sleep 30", "timeout": 3},
|
|
54
|
+
{"cwd": "/tmp"},
|
|
55
|
+
)
|
|
56
|
+
assert "timed out" in out
|
|
57
|
+
assert "IMPORTANT_RESULT" in out and "line2" in out
|
|
58
|
+
assert "output before timeout" in out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_timeout_no_output_stays_clean():
|
|
62
|
+
from drydock.tools import tool_bash
|
|
63
|
+
out = tool_bash({"command": "sleep 30", "timeout": 2}, {"cwd": "/tmp"})
|
|
64
|
+
assert "timed out" in out and "output before timeout" not in out
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Bash output is sanitized before the model sees it: ANSI escape sequences
|
|
2
|
+
(colour/cursor control some tools emit even to a pipe) are stripped, and NUL
|
|
3
|
+
bytes (which trip some LLM servers' JSON handling) are dropped. Text/tabs/
|
|
4
|
+
newlines/Unicode are preserved. Found by probing the output I/O path."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from drydock.tools import tool_bash, _sanitize_bash_output
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.parametrize("raw,clean", [
|
|
13
|
+
("\x1b[31mRED\x1b[0m normal", "RED normal"), # SGR colour
|
|
14
|
+
("\x1b[2K\x1b[1Gline", "line"), # cursor control
|
|
15
|
+
("\x1b]0;window title\x07keep", "keep"), # OSC title
|
|
16
|
+
("a\x00b\x00c", "abc"), # NUL bytes
|
|
17
|
+
("plain\ttext\nline2 🚢⚓", "plain\ttext\nline2 🚢⚓"), # preserved
|
|
18
|
+
])
|
|
19
|
+
def test_sanitizer(raw, clean):
|
|
20
|
+
assert _sanitize_bash_output(raw) == clean
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_end_to_end_forced_color_stripped():
|
|
24
|
+
out = tool_bash({"command": r"printf '\x1b[31mRED\x1b[0m done\n'", "timeout": 8}, {"cwd": "/tmp"})
|
|
25
|
+
assert "RED done" in out and "\x1b" not in out
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_end_to_end_nul_dropped():
|
|
29
|
+
out = tool_bash({"command": r"printf 'a\x00b\x00c'", "timeout": 8}, {"cwd": "/tmp"})
|
|
30
|
+
assert "\x00" not in out and "abc" in out
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_valid_unicode_survives():
|
|
34
|
+
out = tool_bash({"command": r"printf '\xf0\x9f\x9a\xa2 ship\n'", "timeout": 8}, {"cwd": "/tmp"})
|
|
35
|
+
assert "🚢" 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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|