drydock-cli 3.0.77__tar.gz → 3.0.79__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.77/drydock_cli.egg-info → drydock_cli-3.0.79}/PKG-INFO +9 -4
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/README.md +8 -3
- drydock_cli-3.0.79/drydock/poam.py +73 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/providers.py +41 -19
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/tools/__init__.py +43 -4
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/tui/app.py +30 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/tuning.py +8 -4
- {drydock_cli-3.0.77 → drydock_cli-3.0.79/drydock_cli.egg-info}/PKG-INFO +9 -4
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock_cli.egg-info/SOURCES.txt +3 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/pyproject.toml +1 -1
- drydock_cli-3.0.79/tests/test_poam.py +50 -0
- drydock_cli-3.0.79/tests/test_viewimage.py +58 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/LICENSE +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/NOTICE +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/agent.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/cci.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/cli.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/config.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/detect.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/extract.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/guards.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/skills.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/stig.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock/web.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/setup.cfg +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_config.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.77 → drydock_cli-3.0.79}/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.
|
|
3
|
+
Version: 3.0.79
|
|
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
|
|
@@ -61,8 +61,9 @@ A full agentic CLI harness — every tool below is clean-room and dependency-fre
|
|
|
61
61
|
`Edit`, `Bash`, `Glob`, `Grep`.
|
|
62
62
|
- **Vision (multimodal)** — reference an image path in your message (a `.png`/
|
|
63
63
|
`.jpg` screenshot, mockup, or diagram) and it's attached for a vision-capable
|
|
64
|
-
model to *see
|
|
65
|
-
|
|
64
|
+
model to *see*; the agent can also call the `ViewImage` tool to look at an
|
|
65
|
+
image it discovers on its own — describe a UI, read text off a screenshot,
|
|
66
|
+
debug a diagram. Works with any `--mmproj` server (e.g. llama.cpp + a vision model).
|
|
66
67
|
- **Version control** — `GitStatus`, `GitDiff`, `GitLog`, `GitCommit`
|
|
67
68
|
(structured + truncated; commit is local and reversible).
|
|
68
69
|
- **Internet** — `WebSearch` + `WebFetch` (DuckDuckGo; offline-safe).
|
|
@@ -166,13 +167,17 @@ compatible checklist — entirely local (hostnames, IPs, and findings are CUI):
|
|
|
166
167
|
/stig app.ckl open # list the open findings
|
|
167
168
|
/stig-remediate app.ckl SV-900010r1_rule # write an idempotent fix script
|
|
168
169
|
/stig graph app.ckl # ingest + auto-link rules → NIST controls
|
|
170
|
+
/stig poam app.ckl # eMASS POA&M CSV of the open findings
|
|
169
171
|
```
|
|
170
172
|
|
|
171
173
|
`/stig new` parses the XCCDF benchmark (validated against the full 286-rule
|
|
172
174
|
Application STIG); `/stig-assess` reads your evidence and writes status +
|
|
173
175
|
finding-details back in place; `/stig graph` builds `STIG`/`STIG-Rule` nodes and
|
|
174
176
|
**auto-links each rule to its NIST 800-53 control** through DISA's CCI map
|
|
175
|
-
(`Control —SATISFIED_BY→ rule`), fetched once and cached offline.
|
|
177
|
+
(`Control —SATISFIED_BY→ rule`), fetched once and cached offline. `/stig poam`
|
|
178
|
+
exports the open findings to a deterministic **eMASS POA&M CSV** — Control (from
|
|
179
|
+
the CCI map), Vulnerability Description, `POA&M Status=Ongoing`, Milestone (the
|
|
180
|
+
Fix Text), and Severity (CAT I/II/III → High/Moderate/Low) — no LLM, stdlib only.
|
|
176
181
|
|
|
177
182
|
## Install
|
|
178
183
|
|
|
@@ -37,8 +37,9 @@ A full agentic CLI harness — every tool below is clean-room and dependency-fre
|
|
|
37
37
|
`Edit`, `Bash`, `Glob`, `Grep`.
|
|
38
38
|
- **Vision (multimodal)** — reference an image path in your message (a `.png`/
|
|
39
39
|
`.jpg` screenshot, mockup, or diagram) and it's attached for a vision-capable
|
|
40
|
-
model to *see
|
|
41
|
-
|
|
40
|
+
model to *see*; the agent can also call the `ViewImage` tool to look at an
|
|
41
|
+
image it discovers on its own — describe a UI, read text off a screenshot,
|
|
42
|
+
debug a diagram. Works with any `--mmproj` server (e.g. llama.cpp + a vision model).
|
|
42
43
|
- **Version control** — `GitStatus`, `GitDiff`, `GitLog`, `GitCommit`
|
|
43
44
|
(structured + truncated; commit is local and reversible).
|
|
44
45
|
- **Internet** — `WebSearch` + `WebFetch` (DuckDuckGo; offline-safe).
|
|
@@ -142,13 +143,17 @@ compatible checklist — entirely local (hostnames, IPs, and findings are CUI):
|
|
|
142
143
|
/stig app.ckl open # list the open findings
|
|
143
144
|
/stig-remediate app.ckl SV-900010r1_rule # write an idempotent fix script
|
|
144
145
|
/stig graph app.ckl # ingest + auto-link rules → NIST controls
|
|
146
|
+
/stig poam app.ckl # eMASS POA&M CSV of the open findings
|
|
145
147
|
```
|
|
146
148
|
|
|
147
149
|
`/stig new` parses the XCCDF benchmark (validated against the full 286-rule
|
|
148
150
|
Application STIG); `/stig-assess` reads your evidence and writes status +
|
|
149
151
|
finding-details back in place; `/stig graph` builds `STIG`/`STIG-Rule` nodes and
|
|
150
152
|
**auto-links each rule to its NIST 800-53 control** through DISA's CCI map
|
|
151
|
-
(`Control —SATISFIED_BY→ rule`), fetched once and cached offline.
|
|
153
|
+
(`Control —SATISFIED_BY→ rule`), fetched once and cached offline. `/stig poam`
|
|
154
|
+
exports the open findings to a deterministic **eMASS POA&M CSV** — Control (from
|
|
155
|
+
the CCI map), Vulnerability Description, `POA&M Status=Ongoing`, Milestone (the
|
|
156
|
+
Fix Text), and Severity (CAT I/II/III → High/Moderate/Low) — no LLM, stdlib only.
|
|
152
157
|
|
|
153
158
|
## Install
|
|
154
159
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""eMASS-compatible POA&M (Plan of Action & Milestones) export from STIG findings.
|
|
2
|
+
|
|
3
|
+
Deterministic and stdlib-only: takes the OPEN findings in a parsed checklist,
|
|
4
|
+
maps each to its NIST SP 800-53 control (via the DISA CCI map in cci.py) and its
|
|
5
|
+
eMASS severity (STIG CAT I/II/III), and writes the standard eMASS POA&M CSV. No
|
|
6
|
+
LLM in this path — it's a faithful transform of facts already in the checklist +
|
|
7
|
+
the CCI map, so it's reproducible and ungameable. Complements the `/rmf-poam`
|
|
8
|
+
skill (which writes a narrative entry); this is the bulk machine-importable CSV.
|
|
9
|
+
|
|
10
|
+
All logic original to Drydock.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import csv
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
# STIG severity (CAT I/II/III) → eMASS Severity + the raw CAT label.
|
|
18
|
+
_EMASS_SEVERITY = {"high": "High", "medium": "Moderate", "low": "Low"}
|
|
19
|
+
_CAT = {"high": "CAT I", "medium": "CAT II", "low": "CAT III"}
|
|
20
|
+
|
|
21
|
+
# eMASS POA&M columns (the prompt's required fields + the standard companions
|
|
22
|
+
# eMASS expects so the CSV imports cleanly).
|
|
23
|
+
EMASS_HEADERS = [
|
|
24
|
+
"Control",
|
|
25
|
+
"Vulnerability Description",
|
|
26
|
+
"Source Identifying Vulnerability",
|
|
27
|
+
"POA&M Status",
|
|
28
|
+
"Milestone Description",
|
|
29
|
+
"Severity",
|
|
30
|
+
"Raw Severity",
|
|
31
|
+
"Comments",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def poam_rows(checklist, cci_map: dict | None = None) -> list[dict]:
|
|
36
|
+
"""One eMASS POA&M row per OPEN finding. `cci_map` (CCI→control) populates the
|
|
37
|
+
Control column; missing mappings are marked '(unmapped CCI)' rather than
|
|
38
|
+
dropped, so nothing is silently lost."""
|
|
39
|
+
cci_map = cci_map or {}
|
|
40
|
+
rows: list[dict] = []
|
|
41
|
+
for r in checklist.rules:
|
|
42
|
+
if r.status != "open":
|
|
43
|
+
continue
|
|
44
|
+
ctl = cci_map.get(r.cci, "")
|
|
45
|
+
control = ctl.upper() if ctl else (f"(unmapped {r.cci})" if r.cci else "(no CCI)")
|
|
46
|
+
rows.append({
|
|
47
|
+
"Control": control,
|
|
48
|
+
"Vulnerability Description": f"{r.group_id} ({r.rule_id}): {r.title}".strip(),
|
|
49
|
+
"Source Identifying Vulnerability": r.rule_id or r.group_id,
|
|
50
|
+
"POA&M Status": "Ongoing",
|
|
51
|
+
"Milestone Description": (r.fix_text or "").strip() or "See STIG Fix Text.",
|
|
52
|
+
"Severity": _EMASS_SEVERITY.get(r.severity, "Moderate"),
|
|
53
|
+
"Raw Severity": _CAT.get(r.severity, "CAT II"),
|
|
54
|
+
"Comments": (r.finding_details or "").strip(),
|
|
55
|
+
})
|
|
56
|
+
return rows
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def write_csv(rows: list[dict], path: str | Path) -> int:
|
|
60
|
+
"""Write POA&M rows to an eMASS-headered CSV. Returns the row count."""
|
|
61
|
+
with open(path, "w", newline="", encoding="utf-8") as f:
|
|
62
|
+
w = csv.DictWriter(f, fieldnames=EMASS_HEADERS)
|
|
63
|
+
w.writeheader()
|
|
64
|
+
for row in rows:
|
|
65
|
+
w.writerow({h: row.get(h, "") for h in EMASS_HEADERS})
|
|
66
|
+
return len(rows)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def export(checklist, cci_map: dict | None, out_path: str | Path) -> dict:
|
|
70
|
+
"""Build + write the POA&M CSV for a checklist's open findings."""
|
|
71
|
+
rows = poam_rows(checklist, cci_map)
|
|
72
|
+
write_csv(rows, out_path)
|
|
73
|
+
return {"rows": len(rows), "path": str(out_path)}
|
|
@@ -279,32 +279,47 @@ def detect_image_paths(content) -> list[str]:
|
|
|
279
279
|
return seen
|
|
280
280
|
|
|
281
281
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
at the API boundary."""
|
|
289
|
-
seen = detect_image_paths(content)
|
|
290
|
-
if not seen:
|
|
291
|
-
return content
|
|
282
|
+
_MAX_IMAGE_BYTES = 20_000_000 # don't base64 a >20MB file into a request
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _image_url_block(path: str) -> dict | None:
|
|
286
|
+
"""A base64 data-URL image_url block for an on-disk image, or None if it
|
|
287
|
+
can't be read / is too large."""
|
|
292
288
|
import base64
|
|
293
289
|
import os
|
|
294
290
|
|
|
291
|
+
try:
|
|
292
|
+
if os.path.getsize(path) > _MAX_IMAGE_BYTES:
|
|
293
|
+
return None
|
|
294
|
+
with open(path, "rb") as f:
|
|
295
|
+
b64 = base64.b64encode(f.read()).decode()
|
|
296
|
+
except OSError:
|
|
297
|
+
return None
|
|
298
|
+
mime = _IMAGE_MIME.get(os.path.splitext(path)[1].lower(), "image/png")
|
|
299
|
+
return {"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _content_with_images(content):
|
|
303
|
+
"""Turn text that references on-disk image paths into a multimodal content
|
|
304
|
+
list ([text, image_url…]); text without resolvable images passes through
|
|
305
|
+
unchanged. Used for BOTH user messages (the user names an image) and
|
|
306
|
+
ViewImage tool results (the AGENT chose to view one) — built ONLY here at the
|
|
307
|
+
API boundary so display/compaction/token-counting still see plain strings."""
|
|
308
|
+
seen = detect_image_paths(content)
|
|
309
|
+
if not seen:
|
|
310
|
+
return content
|
|
295
311
|
blocks: list[dict] = [{"type": "text", "text": content}]
|
|
296
312
|
for p in seen:
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
b64 = base64.b64encode(f.read()).decode()
|
|
301
|
-
except OSError:
|
|
302
|
-
continue
|
|
303
|
-
blocks.append({"type": "image_url",
|
|
304
|
-
"image_url": {"url": f"data:{mime};base64,{b64}"}})
|
|
313
|
+
block = _image_url_block(p)
|
|
314
|
+
if block:
|
|
315
|
+
blocks.append(block)
|
|
305
316
|
return blocks if len(blocks) > 1 else content
|
|
306
317
|
|
|
307
318
|
|
|
319
|
+
# Back-compat alias (user-message vision).
|
|
320
|
+
_user_content_with_images = _content_with_images
|
|
321
|
+
|
|
322
|
+
|
|
308
323
|
def messages_to_openai(messages: list, system: str) -> list:
|
|
309
324
|
"""Convert neutral messages to OpenAI API format."""
|
|
310
325
|
# value type is mixed (str content, multimodal list content, tool_calls list)
|
|
@@ -331,10 +346,17 @@ def messages_to_openai(messages: list, system: str) -> list:
|
|
|
331
346
|
]
|
|
332
347
|
result.append(msg)
|
|
333
348
|
elif role == "tool":
|
|
349
|
+
content = m["content"]
|
|
350
|
+
# Agent-side vision: a ViewImage result names an image the agent chose
|
|
351
|
+
# to look at — attach it so the model SEES it (servers read images
|
|
352
|
+
# from tool messages too, verified). Only ViewImage, so an unrelated
|
|
353
|
+
# tool mentioning a .png path never balloons the request.
|
|
354
|
+
if m.get("name") == "ViewImage" and isinstance(content, str):
|
|
355
|
+
content = _content_with_images(content)
|
|
334
356
|
result.append({
|
|
335
357
|
"role": "tool",
|
|
336
358
|
"tool_call_id": m["tool_call_id"],
|
|
337
|
-
"content":
|
|
359
|
+
"content": content,
|
|
338
360
|
})
|
|
339
361
|
return result
|
|
340
362
|
|
|
@@ -83,6 +83,17 @@ SCHEMAS = [
|
|
|
83
83
|
"required": ["file_path"],
|
|
84
84
|
},
|
|
85
85
|
},
|
|
86
|
+
{
|
|
87
|
+
"name": "ViewImage",
|
|
88
|
+
"description": "Look at an image file with your vision — use this when you need to SEE a screenshot, mockup, diagram, photo, or rendered output (.png/.jpg/.jpeg/.gif/.webp/.bmp). The image is shown to you so you can describe it, read text from it, or debug it. (Reading an image with the Read tool gives binary garbage; use ViewImage instead.)",
|
|
89
|
+
"input_schema": {
|
|
90
|
+
"type": "object",
|
|
91
|
+
"properties": {
|
|
92
|
+
"path": {"type": "string", "description": "Path to the image file"},
|
|
93
|
+
},
|
|
94
|
+
"required": ["path"],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
86
97
|
{
|
|
87
98
|
"name": "Write",
|
|
88
99
|
"description": "Write content to a file, creating parent directories as needed.",
|
|
@@ -474,6 +485,33 @@ def _file_index(lines: list[str], fp: str) -> str:
|
|
|
474
485
|
return f"{head}\nKey locations ({len(anchors)}):\n{body}"
|
|
475
486
|
|
|
476
487
|
|
|
488
|
+
_IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp")
|
|
489
|
+
_MAX_VIEW_IMAGE_BYTES = 20_000_000
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def tool_viewimage(params: dict, config: dict) -> str:
|
|
493
|
+
"""Validate an image path and return a result that names it; the API boundary
|
|
494
|
+
(providers.messages_to_openai) attaches the actual image to THIS tool result
|
|
495
|
+
so the vision model sees it. Returns a plain error string on any problem
|
|
496
|
+
(never an attachable path) so a bad call can't try to attach nothing."""
|
|
497
|
+
raw = (params.get("path") or params.get("file_path") or "").strip()
|
|
498
|
+
if not raw:
|
|
499
|
+
return "Error: ViewImage needs a `path` to an image file."
|
|
500
|
+
fp = _resolve_path(raw, config)
|
|
501
|
+
if os.path.splitext(fp)[1].lower() not in _IMAGE_EXTS:
|
|
502
|
+
return (f"Error: {raw} is not a supported image. ViewImage handles "
|
|
503
|
+
f"{', '.join(_IMAGE_EXTS)}.")
|
|
504
|
+
if not os.path.isfile(fp):
|
|
505
|
+
return f"Error: no image file at {raw} (resolved to {fp})."
|
|
506
|
+
size = os.path.getsize(fp)
|
|
507
|
+
if size > _MAX_VIEW_IMAGE_BYTES:
|
|
508
|
+
return f"Error: {raw} is {size // 1_000_000}MB — too large to view (limit 20MB)."
|
|
509
|
+
kind = os.path.splitext(fp)[1].lstrip(".").upper()
|
|
510
|
+
# The absolute path in this text is what the API boundary attaches.
|
|
511
|
+
return (f"Loaded image {fp} ({kind}, {size // 1024 or 1}KB) — it is now visible "
|
|
512
|
+
"to you. Describe it, read any text in it, or use it to answer the task.")
|
|
513
|
+
|
|
514
|
+
|
|
477
515
|
def tool_read(params: dict, config: dict) -> str:
|
|
478
516
|
fp = _resolve_path(params["file_path"], config)
|
|
479
517
|
limit = params.get("limit") # None = caller didn't specify a window
|
|
@@ -1010,7 +1048,7 @@ def tool_todo(params: dict, config: dict) -> str:
|
|
|
1010
1048
|
# Tools a sub-agent may use. Excludes Write/Edit/todo (no silent mutations the
|
|
1011
1049
|
# parent can't see) and `task` itself (no recursion). Bash is included for real
|
|
1012
1050
|
# exploration (ls/cat/grep/find/git log) and is bounded by the same bash_safety.
|
|
1013
|
-
SUBAGENT_TOOLS = ("Read", "Glob", "Grep", "Bash")
|
|
1051
|
+
SUBAGENT_TOOLS = ("Read", "ViewImage", "Glob", "Grep", "Bash")
|
|
1014
1052
|
|
|
1015
1053
|
_SUBAGENT_SYSTEM = (
|
|
1016
1054
|
"You are a focused exploration sub-agent inside Drydock. You have read-only "
|
|
@@ -1428,7 +1466,8 @@ def register_all():
|
|
|
1428
1466
|
for schema in SCHEMAS:
|
|
1429
1467
|
name = schema["name"]
|
|
1430
1468
|
func = {
|
|
1431
|
-
"Read": tool_read, "
|
|
1469
|
+
"Read": tool_read, "ViewImage": tool_viewimage,
|
|
1470
|
+
"Write": tool_write, "Edit": tool_edit,
|
|
1432
1471
|
"Bash": tool_bash, "Glob": tool_glob, "Grep": tool_grep,
|
|
1433
1472
|
"todo": tool_todo, "task": tool_task, "Dispatch": tool_dispatch,
|
|
1434
1473
|
"Knowledge": tool_knowledge,
|
|
@@ -1441,8 +1480,8 @@ def register_all():
|
|
|
1441
1480
|
# Read-only w.r.t. the parent's files (GitStatus/Diff/Log inspect only;
|
|
1442
1481
|
# GitCommit + GraphAdd write).
|
|
1443
1482
|
read_only = name in (
|
|
1444
|
-
"Read", "Glob", "Grep", "task", "Dispatch", "Knowledge",
|
|
1445
|
-
"StigRules", "StigRule",
|
|
1483
|
+
"Read", "ViewImage", "Glob", "Grep", "task", "Dispatch", "Knowledge",
|
|
1484
|
+
"GraphQuery", "StigRules", "StigRule",
|
|
1446
1485
|
"WebSearch", "WebFetch", "GitStatus", "GitDiff", "GitLog",
|
|
1447
1486
|
)
|
|
1448
1487
|
register(ToolDef(name=name, schema=schema, func=func, read_only=read_only))
|
|
@@ -547,6 +547,7 @@ class DrydockApp(App):
|
|
|
547
547
|
" /stig new <benchmark-xccdf.xml> [out.ckl] — blank .ckl from a STIG\n"
|
|
548
548
|
" /stig <path.ckl|.cklb> [status] — summary / list by status\n"
|
|
549
549
|
" /stig graph <path.ckl> — ingest into the RMF graph\n"
|
|
550
|
+
" /stig poam <path.ckl> [out.csv] — eMASS POA&M CSV (open findings)\n"
|
|
550
551
|
"Assess: /loop <n> /stig-assess <path>")
|
|
551
552
|
return
|
|
552
553
|
import os as _os
|
|
@@ -569,6 +570,15 @@ class DrydockApp(App):
|
|
|
569
570
|
f" /graphrag build <app-docs> · /loop {len(cl.rules)} /stig-assess {out}"
|
|
570
571
|
)
|
|
571
572
|
return
|
|
573
|
+
# /stig poam <path> [out.csv] — export open findings to an eMASS POA&M CSV
|
|
574
|
+
if parts[0].lower() == "poam" and len(parts) > 1:
|
|
575
|
+
cp = _abs(parts[1])
|
|
576
|
+
out = _abs(parts[2]) if len(parts) > 2 else \
|
|
577
|
+
_os.path.splitext(cp)[0] + "_poam.csv"
|
|
578
|
+
self._info("Building the eMASS POA&M CSV (mapping open findings to NIST "
|
|
579
|
+
"controls via the CCI map)…")
|
|
580
|
+
self.run_worker(lambda: self._stig_poam(cwd, cp, out), thread=True)
|
|
581
|
+
return
|
|
572
582
|
# /stig graph <path> — ingest the checklist into the RMF typed graph
|
|
573
583
|
if parts[0].lower() == "graph" and len(parts) > 1:
|
|
574
584
|
gp = parts[1] if _os.path.isabs(parts[1]) else _os.path.join(cwd, parts[1])
|
|
@@ -640,6 +650,26 @@ class DrydockApp(App):
|
|
|
640
650
|
"catalog download; after that it works offline.)")
|
|
641
651
|
self.call_from_thread(self._info, msg)
|
|
642
652
|
|
|
653
|
+
def _stig_poam(self, cwd: str, path: str, out: str) -> None:
|
|
654
|
+
"""Worker: export a checklist's open findings to an eMASS POA&M CSV,
|
|
655
|
+
pulling each finding's NIST control from the CCI map. Deterministic."""
|
|
656
|
+
from drydock import cci, poam, stig
|
|
657
|
+
|
|
658
|
+
try:
|
|
659
|
+
cl = stig.load(path)
|
|
660
|
+
cci_map = cci.load_map(cwd) # offline-safe ({} → Controls show '(unmapped)')
|
|
661
|
+
r = poam.export(cl, cci_map, out)
|
|
662
|
+
except Exception as e: # noqa: BLE001
|
|
663
|
+
self.call_from_thread(self._mount, ErrorMessage(f"could not build POA&M: {e}"))
|
|
664
|
+
return
|
|
665
|
+
note = "" if cci_map else " (CCI map unavailable offline — Control column shows '(unmapped)'; re-run online to populate it)"
|
|
666
|
+
self.call_from_thread(
|
|
667
|
+
self._info,
|
|
668
|
+
f"✓ Wrote {r['rows']} POA&M row(s) for the open findings to {r['path']} "
|
|
669
|
+
f"(eMASS headers: Control · Vulnerability Description · POA&M Status=Ongoing · "
|
|
670
|
+
f"Milestone · Severity).{note}"
|
|
671
|
+
)
|
|
672
|
+
|
|
643
673
|
def _stig_graph(self, cwd: str, path: str) -> None:
|
|
644
674
|
"""Worker-thread body: ingest a checklist into the RMF graph, auto-linking
|
|
645
675
|
rules to NIST controls via the DISA CCI map. Reports back on the UI."""
|
|
@@ -93,8 +93,10 @@ _DEFAULT_SYSTEM_PROMPT = (
|
|
|
93
93
|
"every step is done. Don't write a TODO.md file for this.\n"
|
|
94
94
|
"You have VISION: when the user references an image file path (.png/.jpg/"
|
|
95
95
|
".jpeg/.gif/.webp/.bmp on disk — a screenshot, mockup, diagram, or photo), it "
|
|
96
|
-
"is attached and you can SEE it;
|
|
97
|
-
"
|
|
96
|
+
"is attached and you can SEE it; you can also call the `ViewImage` tool to "
|
|
97
|
+
"look at any image you find yourself (Read gives binary garbage for images — "
|
|
98
|
+
"use ViewImage). Describe it, read text from it, or debug with it — never "
|
|
99
|
+
"claim you cannot view images."
|
|
98
100
|
)
|
|
99
101
|
|
|
100
102
|
# Short, imperative prompt. Small local models do better with "act now" than
|
|
@@ -119,8 +121,10 @@ _GEMMA_SYSTEM_PROMPT = (
|
|
|
119
121
|
"write a TODO.md file for this.\n"
|
|
120
122
|
"You have VISION: when the user's message references an image file path "
|
|
121
123
|
"(.png/.jpg/.jpeg/.gif/.webp/.bmp that exists on disk — a screenshot, mockup, "
|
|
122
|
-
"diagram, or photo), that image is attached and you can SEE it.
|
|
123
|
-
"
|
|
124
|
+
"diagram, or photo), that image is attached and you can SEE it. You can also "
|
|
125
|
+
"call the `ViewImage` tool to look at any image you find yourself (Read gives "
|
|
126
|
+
"binary garbage for images — use ViewImage). Describe it, read text from it, "
|
|
127
|
+
"or use it to debug — never claim you cannot view images."
|
|
124
128
|
)
|
|
125
129
|
|
|
126
130
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: drydock-cli
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.79
|
|
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
|
|
@@ -61,8 +61,9 @@ A full agentic CLI harness — every tool below is clean-room and dependency-fre
|
|
|
61
61
|
`Edit`, `Bash`, `Glob`, `Grep`.
|
|
62
62
|
- **Vision (multimodal)** — reference an image path in your message (a `.png`/
|
|
63
63
|
`.jpg` screenshot, mockup, or diagram) and it's attached for a vision-capable
|
|
64
|
-
model to *see
|
|
65
|
-
|
|
64
|
+
model to *see*; the agent can also call the `ViewImage` tool to look at an
|
|
65
|
+
image it discovers on its own — describe a UI, read text off a screenshot,
|
|
66
|
+
debug a diagram. Works with any `--mmproj` server (e.g. llama.cpp + a vision model).
|
|
66
67
|
- **Version control** — `GitStatus`, `GitDiff`, `GitLog`, `GitCommit`
|
|
67
68
|
(structured + truncated; commit is local and reversible).
|
|
68
69
|
- **Internet** — `WebSearch` + `WebFetch` (DuckDuckGo; offline-safe).
|
|
@@ -166,13 +167,17 @@ compatible checklist — entirely local (hostnames, IPs, and findings are CUI):
|
|
|
166
167
|
/stig app.ckl open # list the open findings
|
|
167
168
|
/stig-remediate app.ckl SV-900010r1_rule # write an idempotent fix script
|
|
168
169
|
/stig graph app.ckl # ingest + auto-link rules → NIST controls
|
|
170
|
+
/stig poam app.ckl # eMASS POA&M CSV of the open findings
|
|
169
171
|
```
|
|
170
172
|
|
|
171
173
|
`/stig new` parses the XCCDF benchmark (validated against the full 286-rule
|
|
172
174
|
Application STIG); `/stig-assess` reads your evidence and writes status +
|
|
173
175
|
finding-details back in place; `/stig graph` builds `STIG`/`STIG-Rule` nodes and
|
|
174
176
|
**auto-links each rule to its NIST 800-53 control** through DISA's CCI map
|
|
175
|
-
(`Control —SATISFIED_BY→ rule`), fetched once and cached offline.
|
|
177
|
+
(`Control —SATISFIED_BY→ rule`), fetched once and cached offline. `/stig poam`
|
|
178
|
+
exports the open findings to a deterministic **eMASS POA&M CSV** — Control (from
|
|
179
|
+
the CCI map), Vulnerability Description, `POA&M Status=Ongoing`, Milestone (the
|
|
180
|
+
Fix Text), and Severity (CAT I/II/III → High/Moderate/Low) — no LLM, stdlib only.
|
|
176
181
|
|
|
177
182
|
## Install
|
|
178
183
|
|
|
@@ -17,6 +17,7 @@ drydock/graphrag.py
|
|
|
17
17
|
drydock/guards.py
|
|
18
18
|
drydock/loop_detect.py
|
|
19
19
|
drydock/mcp.py
|
|
20
|
+
drydock/poam.py
|
|
20
21
|
drydock/providers.py
|
|
21
22
|
drydock/rmf.py
|
|
22
23
|
drydock/rmf_graph.py
|
|
@@ -73,6 +74,7 @@ tests/test_loop_detect.py
|
|
|
73
74
|
tests/test_mcp.py
|
|
74
75
|
tests/test_oneshot_unreachable.py
|
|
75
76
|
tests/test_plan_autocontinue.py
|
|
77
|
+
tests/test_poam.py
|
|
76
78
|
tests/test_providers_unreachable.py
|
|
77
79
|
tests/test_read_index.py
|
|
78
80
|
tests/test_rmf.py
|
|
@@ -91,6 +93,7 @@ tests/test_tool_arg_parsing.py
|
|
|
91
93
|
tests/test_tools_undo.py
|
|
92
94
|
tests/test_tui.py
|
|
93
95
|
tests/test_tuning.py
|
|
96
|
+
tests/test_viewimage.py
|
|
94
97
|
tests/test_vision_input.py
|
|
95
98
|
tests/test_web_tools.py
|
|
96
99
|
tests/test_xccdf.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.79"
|
|
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,50 @@
|
|
|
1
|
+
"""eMASS POA&M CSV export from open STIG findings (deterministic)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import csv
|
|
5
|
+
|
|
6
|
+
from drydock import poam, stig
|
|
7
|
+
|
|
8
|
+
_CKL = ('<?xml version="1.0"?><CHECKLIST><ASSET><HOST_NAME>h1</HOST_NAME></ASSET><STIGS><iSTIG><STIG_INFO></STIG_INFO>'
|
|
9
|
+
'<VULN><STIG_DATA><VULN_ATTRIBUTE>Vuln_Num</VULN_ATTRIBUTE><ATTRIBUTE_DATA>V-1</ATTRIBUTE_DATA></STIG_DATA>'
|
|
10
|
+
'<STIG_DATA><VULN_ATTRIBUTE>Rule_ID</VULN_ATTRIBUTE><ATTRIBUTE_DATA>SV-1</ATTRIBUTE_DATA></STIG_DATA>'
|
|
11
|
+
'<STIG_DATA><VULN_ATTRIBUTE>Rule_Title</VULN_ATTRIBUTE><ATTRIBUTE_DATA>Disable debug</ATTRIBUTE_DATA></STIG_DATA>'
|
|
12
|
+
'<STIG_DATA><VULN_ATTRIBUTE>Severity</VULN_ATTRIBUTE><ATTRIBUTE_DATA>high</ATTRIBUTE_DATA></STIG_DATA>'
|
|
13
|
+
'<STIG_DATA><VULN_ATTRIBUTE>Fix_Text</VULN_ATTRIBUTE><ATTRIBUTE_DATA>Set debug=false.</ATTRIBUTE_DATA></STIG_DATA>'
|
|
14
|
+
'<STIG_DATA><VULN_ATTRIBUTE>CCI_REF</VULN_ATTRIBUTE><ATTRIBUTE_DATA>CCI-000366</ATTRIBUTE_DATA></STIG_DATA>'
|
|
15
|
+
'<STATUS>Open</STATUS></VULN>'
|
|
16
|
+
'<VULN><STIG_DATA><VULN_ATTRIBUTE>Vuln_Num</VULN_ATTRIBUTE><ATTRIBUTE_DATA>V-2</ATTRIBUTE_DATA></STIG_DATA>'
|
|
17
|
+
'<STATUS>NotAFinding</STATUS></VULN></iSTIG></STIGS></CHECKLIST>')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_poam_rows_only_open_with_control_and_severity(tmp_path):
|
|
21
|
+
p = tmp_path / "s.ckl"; p.write_text(_CKL)
|
|
22
|
+
cl = stig.load(p)
|
|
23
|
+
rows = poam.poam_rows(cl, {"CCI-000366": "cm-6"})
|
|
24
|
+
assert len(rows) == 1 # only the Open finding
|
|
25
|
+
row = rows[0]
|
|
26
|
+
assert row["Control"] == "CM-6" # from the CCI map
|
|
27
|
+
assert row["Severity"] == "High" and row["Raw Severity"] == "CAT I" # high → CAT I/High
|
|
28
|
+
assert row["POA&M Status"] == "Ongoing"
|
|
29
|
+
assert "Set debug=false." in row["Milestone Description"]
|
|
30
|
+
assert "V-1" in row["Vulnerability Description"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_poam_unmapped_cci_is_marked_not_dropped(tmp_path):
|
|
34
|
+
p = tmp_path / "s.ckl"; p.write_text(_CKL)
|
|
35
|
+
cl = stig.load(p)
|
|
36
|
+
rows = poam.poam_rows(cl, {}) # no map
|
|
37
|
+
assert rows[0]["Control"] == "(unmapped CCI-000366)" # marked, not silently lost
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_write_csv_has_emass_headers(tmp_path):
|
|
41
|
+
p = tmp_path / "s.ckl"; p.write_text(_CKL)
|
|
42
|
+
cl = stig.load(p)
|
|
43
|
+
out = tmp_path / "poam.csv"
|
|
44
|
+
n = poam.export(cl, {"CCI-000366": "cm-6"}, out)["rows"]
|
|
45
|
+
assert n == 1
|
|
46
|
+
with open(out, newline="") as f:
|
|
47
|
+
reader = csv.DictReader(f)
|
|
48
|
+
assert reader.fieldnames == poam.EMASS_HEADERS
|
|
49
|
+
row = next(reader)
|
|
50
|
+
assert row["Control"] == "CM-6" and row["Severity"] == "High"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Agent-side vision: the ViewImage tool + its API-boundary attachment.
|
|
2
|
+
The agent can choose to SEE an image it discovers (not just user-referenced
|
|
3
|
+
ones); the image rides back on the ViewImage tool result."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import struct
|
|
7
|
+
import zlib
|
|
8
|
+
|
|
9
|
+
import drydock.tools # noqa: F401 — triggers tool registration
|
|
10
|
+
from drydock import tool_registry as reg
|
|
11
|
+
from drydock.providers import messages_to_openai
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _png(path):
|
|
15
|
+
w = h = 8
|
|
16
|
+
raw = b"".join(b"\x00" + bytes([10, 20, 200]) * w for _ in range(h))
|
|
17
|
+
def ch(t, d):
|
|
18
|
+
c = t + d
|
|
19
|
+
return struct.pack(">I", len(d)) + c + struct.pack(">I", zlib.crc32(c) & 0xFFFFFFFF)
|
|
20
|
+
path.write_bytes(b"\x89PNG\r\n\x1a\n"
|
|
21
|
+
+ ch(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0))
|
|
22
|
+
+ ch(b"IDAT", zlib.compress(raw)) + ch(b"IEND", b""))
|
|
23
|
+
return str(path)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_viewimage_validates(tmp_path):
|
|
27
|
+
_png(tmp_path / "a.png")
|
|
28
|
+
cfg = {"cwd": str(tmp_path)}
|
|
29
|
+
assert "now visible to you" in reg.execute("ViewImage", {"path": "a.png"}, cfg)
|
|
30
|
+
assert "no image file" in reg.execute("ViewImage", {"path": "missing.png"}, cfg)
|
|
31
|
+
assert "not a supported image" in reg.execute("ViewImage", {"path": str(tmp_path / "x.txt")}, cfg)
|
|
32
|
+
assert "needs a `path`" in reg.execute("ViewImage", {}, cfg)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_viewimage_is_read_only_and_subagent_visible():
|
|
36
|
+
td = reg.get("ViewImage")
|
|
37
|
+
assert td.read_only is True
|
|
38
|
+
from drydock.tools import SUBAGENT_TOOLS
|
|
39
|
+
assert "ViewImage" in SUBAGENT_TOOLS
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_viewimage_result_becomes_multimodal(tmp_path):
|
|
43
|
+
_png(tmp_path / "shot.png")
|
|
44
|
+
cfg = {"cwd": str(tmp_path)}
|
|
45
|
+
res = reg.execute("ViewImage", {"path": "shot.png"}, cfg)
|
|
46
|
+
out = messages_to_openai([{"role": "tool", "tool_call_id": "c1",
|
|
47
|
+
"name": "ViewImage", "content": res}], "sys")
|
|
48
|
+
content = out[-1]["content"]
|
|
49
|
+
assert isinstance(content, list)
|
|
50
|
+
assert any(b.get("type") == "image_url" for b in content)
|
|
51
|
+
assert content[0]["type"] == "text"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_non_viewimage_tool_with_png_path_stays_text(tmp_path):
|
|
55
|
+
img = _png(tmp_path / "shot.png")
|
|
56
|
+
out = messages_to_openai([{"role": "tool", "tool_call_id": "c2",
|
|
57
|
+
"name": "Grep", "content": f"found {img} in code"}], "sys")
|
|
58
|
+
assert isinstance(out[-1]["content"], str) # no accidental image ballooning
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|