drydock-cli 3.0.92__tar.gz → 3.0.94__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.92/drydock_cli.egg-info → drydock_cli-3.0.94}/PKG-INFO +1 -1
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/tools/__init__.py +22 -2
- {drydock_cli-3.0.92 → drydock_cli-3.0.94/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock_cli.egg-info/SOURCES.txt +2 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/pyproject.toml +1 -1
- drydock_cli-3.0.94/tests/test_bash_binary_output.py +33 -0
- drydock_cli-3.0.94/tests/test_bash_sanitize.py +35 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/LICENSE +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/NOTICE +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/README.md +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/agent.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/cci.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/cli.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/config.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/detect.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/extract.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/guards.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/poam.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/providers.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/skills.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/stig.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock/web.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/setup.cfg +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_config.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.92 → drydock_cli-3.0.94}/tests/test_xccdf.py +0 -0
|
@@ -46,6 +46,20 @@ def _detect_bash() -> str | None:
|
|
|
46
46
|
_BASH_SHELL = _detect_bash()
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
# ANSI escape sequences (CSI colour/cursor, OSC title, and lone two-char escapes).
|
|
50
|
+
# Some tools emit these even to a pipe (--color=always, forced-colour test runners),
|
|
51
|
+
# and they're pure display control — noise that wastes the model's tokens.
|
|
52
|
+
_ANSI_RE = re.compile(r"\x1b(?:\[[0-9;?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-Z\\-_])")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _sanitize_bash_output(text: str) -> str:
|
|
56
|
+
"""Strip ANSI escape sequences and drop NUL bytes from command output. NUL is
|
|
57
|
+
valid UTF-8 (so the errors='replace' decode keeps it) but raw NUL in a JSON
|
|
58
|
+
tool-result trips some LLM servers' tokenizers; ANSI codes are display noise.
|
|
59
|
+
Neither carries information the model needs. Newlines/tabs/Unicode are kept."""
|
|
60
|
+
return _ANSI_RE.sub("", text).replace("\x00", "")
|
|
61
|
+
|
|
62
|
+
|
|
49
63
|
def _collapse_repeated_lines(text: str, run: int = 20) -> str:
|
|
50
64
|
"""Collapse a run of >= `run` IDENTICAL consecutive lines into one line + a
|
|
51
65
|
count. Repetitive output (`yes`, a spinning progress log) tokenizes densely —
|
|
@@ -942,7 +956,13 @@ def tool_bash(params: dict, config: dict) -> str:
|
|
|
942
956
|
# explicitly via a pipe/redirect (echo x | cmd, cmd < file), which
|
|
943
957
|
# overrides this.
|
|
944
958
|
stdin=subprocess.DEVNULL,
|
|
945
|
-
|
|
959
|
+
# errors="replace" so BINARY / non-UTF8 output (cat a binary, a tool
|
|
960
|
+
# emitting raw bytes, gzip to stdout) decodes to replacement chars
|
|
961
|
+
# instead of raising UnicodeDecodeError inside the reader thread —
|
|
962
|
+
# which killed the thread and handed the agent "(no output)", losing
|
|
963
|
+
# even the text parts of mixed output.
|
|
964
|
+
text=True, encoding="utf-8", errors="replace",
|
|
965
|
+
cwd=config.get("cwd"), start_new_session=True,
|
|
946
966
|
)
|
|
947
967
|
config.setdefault("_abort", {})["proc"] = proc
|
|
948
968
|
# Read output in a daemon thread with a HARD byte cap, so memory can't
|
|
@@ -1007,7 +1027,7 @@ def tool_bash(params: dict, config: dict) -> str:
|
|
|
1007
1027
|
# then note if we hit the byte cap. Bounds both RAM (the cap) and context
|
|
1008
1028
|
# tokens (the collapse). Snapshot chunks (list()) in case the reader
|
|
1009
1029
|
# daemon is still appending for a backgrounded child.
|
|
1010
|
-
output = _collapse_repeated_lines("".join(list(chunks)))
|
|
1030
|
+
output = _sanitize_bash_output(_collapse_repeated_lines("".join(list(chunks))))
|
|
1011
1031
|
if backgrounded:
|
|
1012
1032
|
return (output.rstrip() + "\n[a process was left running in the background; "
|
|
1013
1033
|
"the command returned. Check it with a follow-up command.]").lstrip("\n")
|
|
@@ -50,9 +50,11 @@ tests/test_advisor.py
|
|
|
50
50
|
tests/test_approval.py
|
|
51
51
|
tests/test_back_command.py
|
|
52
52
|
tests/test_bash_background.py
|
|
53
|
+
tests/test_bash_binary_output.py
|
|
53
54
|
tests/test_bash_output_bounding.py
|
|
54
55
|
tests/test_bash_process_group.py
|
|
55
56
|
tests/test_bash_safety.py
|
|
57
|
+
tests/test_bash_sanitize.py
|
|
56
58
|
tests/test_bash_shell.py
|
|
57
59
|
tests/test_bash_stdin.py
|
|
58
60
|
tests/test_bash_timeout_network.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.94"
|
|
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,33 @@
|
|
|
1
|
+
"""tool_bash must not choke on binary / non-UTF8 stdout. text=True with the
|
|
2
|
+
default (strict) decode raised UnicodeDecodeError INSIDE the reader thread — the
|
|
3
|
+
thread died and the agent got "(no output)", losing even the text parts of mixed
|
|
4
|
+
output. errors="replace" keeps it non-crashing and preserves the text. Found by
|
|
5
|
+
probing the binary-output I/O path directly."""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from drydock.tools import tool_bash
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _run(cmd):
|
|
12
|
+
return tool_bash({"command": cmd, "timeout": 8}, {"cwd": "/tmp"})
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_invalid_utf8_does_not_crash_and_keeps_text():
|
|
16
|
+
out = _run(r"printf '\xff\xfe hello'")
|
|
17
|
+
assert "hello" in out and "(no output)" not in out
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_mixed_text_and_binary_preserves_text():
|
|
21
|
+
# the readable lines must survive even though raw bytes sit between them
|
|
22
|
+
out = _run(r"echo START; printf '\xc0\xc1\xff'; echo END")
|
|
23
|
+
assert "START" in out and "END" in out
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_binary_file_head_returns_something():
|
|
27
|
+
out = _run("head -c 80 /bin/ls")
|
|
28
|
+
assert out.strip() and "(no output)" not in out and "EXCEPTION" not in out
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_plain_text_unaffected():
|
|
32
|
+
out = _run("echo hello && echo world")
|
|
33
|
+
assert "hello" in out and "world" 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
|