drydock-cli 3.0.80__tar.gz → 3.0.81__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.80/drydock_cli.egg-info → drydock_cli-3.0.81}/PKG-INFO +1 -1
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/agent.py +12 -1
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/compaction.py +10 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/pyproject.toml +1 -1
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_cci.py +16 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_poam.py +30 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_server_probe.py +18 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_viewimage.py +8 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/LICENSE +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/NOTICE +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/README.md +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/cci.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/cli.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/config.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/detect.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/extract.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/guards.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/poam.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/providers.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/skills.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/stig.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/web.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock_cli.egg-info/SOURCES.txt +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/setup.cfg +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_config.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_xccdf.py +0 -0
|
@@ -29,7 +29,9 @@ from drydock.tools import register_all
|
|
|
29
29
|
# emits tool calls as TEXT — which is exactly how the empty-registry regression
|
|
30
30
|
# manifested. register_all() is idempotent.
|
|
31
31
|
register_all()
|
|
32
|
-
from drydock.compaction import
|
|
32
|
+
from drydock.compaction import (
|
|
33
|
+
maybe_compact, emergency_compact, is_context_length_error, is_image_load_error,
|
|
34
|
+
)
|
|
33
35
|
from drydock.loop_detect import LoopTracker
|
|
34
36
|
from drydock.tuning import (
|
|
35
37
|
filter_tool_schemas,
|
|
@@ -184,6 +186,15 @@ def run(
|
|
|
184
186
|
if retries >= 2:
|
|
185
187
|
raise
|
|
186
188
|
yield TextChunk("\n[context limit hit — compacting and retrying...]\n")
|
|
189
|
+
elif is_image_load_error(err):
|
|
190
|
+
# Corrupt/truncated/unsupported image the server couldn't decode —
|
|
191
|
+
# end the turn cleanly rather than dumping the raw 400.
|
|
192
|
+
yield TextChunk(
|
|
193
|
+
"\n[The model server couldn't load an attached image — it may be "
|
|
194
|
+
"corrupt, truncated, or an unsupported format. Try a different image.]\n"
|
|
195
|
+
)
|
|
196
|
+
assistant_turn = None
|
|
197
|
+
break
|
|
187
198
|
else:
|
|
188
199
|
raise
|
|
189
200
|
|
|
@@ -34,6 +34,16 @@ def is_context_length_error(err: str) -> bool:
|
|
|
34
34
|
return "context" in e and ("exceed" in e or "too long" in e or "too large" in e)
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def is_image_load_error(err: str) -> bool:
|
|
38
|
+
"""Whether a provider 400 is the server failing to decode an attached image
|
|
39
|
+
(corrupt / truncated / unsupported), so the agent ends the turn with a clean
|
|
40
|
+
message instead of dumping the raw API error."""
|
|
41
|
+
e = (err or "").lower()
|
|
42
|
+
return "failed to load image" in e or (
|
|
43
|
+
"image" in e and ("invalid_request" in e or "could not" in e or "decode" in e)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
37
47
|
def estimate_tokens(messages: list) -> int:
|
|
38
48
|
"""Rough token estimate: chars / 3.5.
|
|
39
49
|
|
|
@@ -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.81"
|
|
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"
|
|
@@ -80,3 +80,19 @@ def test_load_map_creates_dir_and_caches(tmp_path, monkeypatch):
|
|
|
80
80
|
m = cci.load_map(str(tmp_path))
|
|
81
81
|
assert m["CCI-000366"] == "cm-6" # parsed from the "fetched" list
|
|
82
82
|
assert cci.cache_path(str(tmp_path)).exists() # cached as json
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_parse_cci_tolerates_malformed_items(tmp_path):
|
|
86
|
+
# items with no references, empty references, and non-NIST refs are skipped,
|
|
87
|
+
# not crashed on
|
|
88
|
+
xml = ('<?xml version="1.0"?><cci_list><cci_items>'
|
|
89
|
+
'<cci_item id="CCI-A"></cci_item>' # no references
|
|
90
|
+
'<cci_item id="CCI-B"><references></references></cci_item>'
|
|
91
|
+
'<cci_item id="CCI-C"><references>'
|
|
92
|
+
'<reference title="Some Other Doc" version="1" index="X-9"/></references></cci_item>'
|
|
93
|
+
'<cci_item id="CCI-D"><references>'
|
|
94
|
+
'<reference title="NIST SP 800-53 Revision 5" version="5" index="SI-4"/></references></cci_item>'
|
|
95
|
+
'</cci_items></cci_list>')
|
|
96
|
+
p = tmp_path / "c.xml"; p.write_text(xml)
|
|
97
|
+
m = cci.parse_cci_list(p)
|
|
98
|
+
assert m == {"CCI-D": "si-4"} # only the valid mapping
|
|
@@ -48,3 +48,33 @@ def test_write_csv_has_emass_headers(tmp_path):
|
|
|
48
48
|
assert reader.fieldnames == poam.EMASS_HEADERS
|
|
49
49
|
row = next(reader)
|
|
50
50
|
assert row["Control"] == "CM-6" and row["Severity"] == "High"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_poam_cklb_mixed_severities(tmp_path):
|
|
54
|
+
import json
|
|
55
|
+
data = {"stigs": [{"stig_name": "S", "version": "1", "rules": [
|
|
56
|
+
{"group_id": "V-10", "rule_id": "SV-10", "rule_title": "low one", "severity": "low",
|
|
57
|
+
"status": "open", "fix_text": "fix low", "ccis": ["CCI-1"]},
|
|
58
|
+
{"group_id": "V-11", "rule_id": "SV-11", "rule_title": "med one", "severity": "medium",
|
|
59
|
+
"status": "open", "fix_text": "fix med"},
|
|
60
|
+
{"group_id": "V-12", "rule_id": "SV-12", "rule_title": "clean", "severity": "high",
|
|
61
|
+
"status": "not_a_finding"},
|
|
62
|
+
]}]}
|
|
63
|
+
p = tmp_path / "m.cklb"; p.write_text(json.dumps(data))
|
|
64
|
+
cl = stig.load(p)
|
|
65
|
+
rows = poam.poam_rows(cl, {"CCI-1": "ac-2"})
|
|
66
|
+
assert len(rows) == 2 # 2 open, the NAF excluded
|
|
67
|
+
by_sev = {r["Source Identifying Vulnerability"]: r["Severity"] for r in rows}
|
|
68
|
+
assert by_sev["SV-10"] == "Low" and by_sev["SV-11"] == "Moderate"
|
|
69
|
+
assert rows[0]["Control"] == "AC-2" # CCI-1 mapped
|
|
70
|
+
assert rows[1]["Control"] == "(no CCI)" # no cci on V-11
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_poam_empty_checklist_writes_header_only(tmp_path):
|
|
74
|
+
import json
|
|
75
|
+
p = tmp_path / "e.cklb"; p.write_text(json.dumps({"stigs": [{"rules": []}]}))
|
|
76
|
+
cl = stig.load(p)
|
|
77
|
+
out = tmp_path / "e.csv"
|
|
78
|
+
assert poam.export(cl, {}, out)["rows"] == 0
|
|
79
|
+
lines = out.read_text().strip().splitlines()
|
|
80
|
+
assert len(lines) == 1 and lines[0].startswith("Control,") # header only, valid CSV
|
|
@@ -37,3 +37,21 @@ def test_probe_unreachable_is_none(monkeypatch):
|
|
|
37
37
|
def boom(url, timeout=0): raise OSError("offline")
|
|
38
38
|
monkeypatch.setattr(urllib.request, "urlopen", boom)
|
|
39
39
|
assert providers.probe_server_context("http://localhost:9999/v1") is None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_probe_garbage_and_missing_fields(monkeypatch):
|
|
43
|
+
import urllib.request
|
|
44
|
+
def fake(url, timeout=0):
|
|
45
|
+
return _Resp({"unexpected": "shape"}) # no n_ctx / max_model_len
|
|
46
|
+
monkeypatch.setattr(urllib.request, "urlopen", fake)
|
|
47
|
+
assert providers.probe_server_context("http://localhost:8000/v1") is None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_probe_zero_or_negative_ignored(monkeypatch):
|
|
51
|
+
import urllib.request
|
|
52
|
+
def fake(url, timeout=0):
|
|
53
|
+
if url.endswith("/props"):
|
|
54
|
+
return _Resp({"default_generation_settings": {"n_ctx": 0}})
|
|
55
|
+
return _Resp({"data": [{"max_model_len": -1}]})
|
|
56
|
+
monkeypatch.setattr(urllib.request, "urlopen", fake)
|
|
57
|
+
assert providers.probe_server_context("http://localhost:8000/v1") is None
|
|
@@ -66,3 +66,11 @@ def test_read_on_image_points_to_viewimage(tmp_path):
|
|
|
66
66
|
(tmp_path / "f.txt").write_text("hello\nworld\n")
|
|
67
67
|
txt = reg.execute("Read", {"file_path": str(tmp_path / "f.txt")}, {"cwd": str(tmp_path)})
|
|
68
68
|
assert "hello" in txt and "ViewImage" not in txt
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_image_load_error_classifier():
|
|
72
|
+
from drydock.compaction import is_image_load_error
|
|
73
|
+
assert is_image_load_error("Error code: 400 - {'error': {'message': 'Failed to load image or audio file'}}")
|
|
74
|
+
assert is_image_load_error("invalid_request_error: could not decode image")
|
|
75
|
+
assert not is_image_load_error("rate limit exceeded")
|
|
76
|
+
assert not is_image_load_error("context_length_exceeded")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|