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.
Files changed (101) hide show
  1. {drydock_cli-3.0.80/drydock_cli.egg-info → drydock_cli-3.0.81}/PKG-INFO +1 -1
  2. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/agent.py +12 -1
  3. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/compaction.py +10 -0
  4. {drydock_cli-3.0.80 → drydock_cli-3.0.81/drydock_cli.egg-info}/PKG-INFO +1 -1
  5. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/pyproject.toml +1 -1
  6. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_cci.py +16 -0
  7. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_poam.py +30 -0
  8. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_server_probe.py +18 -0
  9. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_viewimage.py +8 -0
  10. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/LICENSE +0 -0
  11. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/NOTICE +0 -0
  12. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/README.md +0 -0
  13. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/__init__.py +0 -0
  14. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/__main__.py +0 -0
  15. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/bash_safety.py +0 -0
  16. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/builtin_skills/__init__.py +0 -0
  17. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/builtin_skills/rmf-categorize.md +0 -0
  18. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/builtin_skills/rmf-control.md +0 -0
  19. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/builtin_skills/rmf-poam.md +0 -0
  20. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/builtin_skills/rmf-review.md +0 -0
  21. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/builtin_skills/stig-assess.md +0 -0
  22. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/builtin_skills/stig-remediate.md +0 -0
  23. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/cci.py +0 -0
  24. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/cli.py +0 -0
  25. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/config.py +0 -0
  26. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/detect.py +0 -0
  27. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/extract.py +0 -0
  28. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/gittools.py +0 -0
  29. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/graphrag.py +0 -0
  30. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/guards.py +0 -0
  31. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/loop_detect.py +0 -0
  32. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/mcp.py +0 -0
  33. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/poam.py +0 -0
  34. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/providers.py +0 -0
  35. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/rmf.py +0 -0
  36. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/rmf_graph.py +0 -0
  37. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/skills.py +0 -0
  38. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/stig.py +0 -0
  39. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/tool_registry.py +0 -0
  40. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/tools/__init__.py +0 -0
  41. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/tui/__init__.py +0 -0
  42. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/tui/app.py +0 -0
  43. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/tui/approval.py +0 -0
  44. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/tui/messages.py +0 -0
  45. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/tui/widgets.py +0 -0
  46. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/tuning.py +0 -0
  47. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock/web.py +0 -0
  48. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock_cli.egg-info/SOURCES.txt +0 -0
  49. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock_cli.egg-info/dependency_links.txt +0 -0
  50. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock_cli.egg-info/entry_points.txt +0 -0
  51. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock_cli.egg-info/requires.txt +0 -0
  52. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/drydock_cli.egg-info/top_level.txt +0 -0
  53. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/setup.cfg +0 -0
  54. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_approval.py +0 -0
  55. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_back_command.py +0 -0
  56. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_bash_output_bounding.py +0 -0
  57. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_bash_process_group.py +0 -0
  58. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_bash_safety.py +0 -0
  59. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_bash_timeout_network.py +0 -0
  60. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_cli_agents.py +0 -0
  61. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_compact_command.py +0 -0
  62. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_compaction.py +0 -0
  63. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_config.py +0 -0
  64. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_config_migration.py +0 -0
  65. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_context_limit_config.py +0 -0
  66. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_detect.py +0 -0
  67. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_dispatch.py +0 -0
  68. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_e2e_connected.py +0 -0
  69. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_empty_response.py +0 -0
  70. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_extract.py +0 -0
  71. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_failure_loop.py +0 -0
  72. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_first_run_setup.py +0 -0
  73. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_gittools.py +0 -0
  74. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_graphrag.py +0 -0
  75. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_guards_and_tools.py +0 -0
  76. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_hallucinated_tools.py +0 -0
  77. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_leaked_tool_call.py +0 -0
  78. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_loop_detect.py +0 -0
  79. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_mcp.py +0 -0
  80. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_oneshot_unreachable.py +0 -0
  81. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_plan_autocontinue.py +0 -0
  82. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_providers_unreachable.py +0 -0
  83. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_read_index.py +0 -0
  84. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_rmf.py +0 -0
  85. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_rmf_graph.py +0 -0
  86. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_rmf_stig_graph.py +0 -0
  87. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_runaway_repetition.py +0 -0
  88. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_skills.py +0 -0
  89. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_stig.py +0 -0
  90. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_stop.py +0 -0
  91. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_streaming_newlines.py +0 -0
  92. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_subagent.py +0 -0
  93. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_system_prompt_help.py +0 -0
  94. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_todo.py +0 -0
  95. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_tool_arg_parsing.py +0 -0
  96. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_tools_undo.py +0 -0
  97. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_tui.py +0 -0
  98. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_tuning.py +0 -0
  99. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_vision_input.py +0 -0
  100. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_web_tools.py +0 -0
  101. {drydock_cli-3.0.80 → drydock_cli-3.0.81}/tests/test_xccdf.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.80
3
+ Version: 3.0.81
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.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 maybe_compact, emergency_compact, is_context_length_error
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.80
3
+ Version: 3.0.81
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.0
@@ -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.80"
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