drydock-cli 3.0.74__tar.gz → 3.0.76__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.74/drydock_cli.egg-info → drydock_cli-3.0.76}/PKG-INFO +37 -10
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/README.md +36 -9
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/providers.py +30 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/tools/__init__.py +23 -4
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/tui/app.py +28 -4
- {drydock_cli-3.0.74 → drydock_cli-3.0.76/drydock_cli.egg-info}/PKG-INFO +37 -10
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock_cli.egg-info/SOURCES.txt +1 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/pyproject.toml +1 -1
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_dispatch.py +11 -0
- drydock_cli-3.0.76/tests/test_server_probe.py +39 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/LICENSE +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/NOTICE +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/agent.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/cci.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/cli.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/config.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/detect.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/extract.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/guards.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/skills.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/stig.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/web.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/setup.cfg +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_config.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.74 → drydock_cli-3.0.76}/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.76
|
|
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
|
|
@@ -91,10 +91,14 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
|
|
|
91
91
|
| `/mcp` | List connected MCP servers + their tools |
|
|
92
92
|
| `/rmf bootstrap [families]` | Ingest the NIST SP 800-53 catalog (RMF automation) |
|
|
93
93
|
| `/rmf-control` · `/rmf-categorize` · `/rmf-review` · `/rmf-poam` | Bundled RMF skills |
|
|
94
|
+
| `/stig new <xccdf>` | Generate a blank `.ckl` from a DISA STIG benchmark |
|
|
95
|
+
| `/stig <ckl>` · `/stig <ckl> open` | Summarize a checklist · list findings by status |
|
|
96
|
+
| `/stig graph <ckl>` | Ingest a checklist into the RMF graph (auto-links rules→controls via CCI) |
|
|
97
|
+
| `/stig-assess <ckl>` · `/stig-remediate <ckl> <rule>` | Assess a rule vs evidence · write a fix script |
|
|
94
98
|
| `/model` · `/cwd` | Show/set model & endpoint · working directory |
|
|
95
99
|
| `/undo` · `/back` | Revert the last write · rewind the last turn |
|
|
96
|
-
| `/compact` · `/
|
|
97
|
-
| `/help` · `/quit` |
|
|
100
|
+
| `/compact` · `/context [n]` | Shrink context now · view/set the context-window budget |
|
|
101
|
+
| `/status` · `/clear` · `/help` · `/quit` | Session stats · reset · help · exit |
|
|
98
102
|
|
|
99
103
|
### Knowledge base (GraphRAG) — ingesting your documents
|
|
100
104
|
|
|
@@ -107,9 +111,11 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
|
|
|
107
111
|
|
|
108
112
|
Once built, the agent **automatically** retrieves from it (read-only `Knowledge`
|
|
109
113
|
tool) when a question touches your material. Ingests text formats
|
|
110
|
-
(`.md .txt .py .js .json .yaml .sql …`) **
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
(`.md .txt .py .js .json .yaml .sql …`), **PDF and Word (`.docx`)**, and **STIG
|
|
115
|
+
checklists (`.ckl`/`.cklb`)** — checklists are flattened to per-rule findings so
|
|
116
|
+
you can ask "which findings are open?". `.docx`/`.ckl` need nothing extra; PDF
|
|
117
|
+
uses the `pdftotext` binary (poppler) if present, else `pip install
|
|
118
|
+
drydock-cli[pdf]` (pypdf). The index is a single JSON at
|
|
113
119
|
`<project>/.drydock/graphrag.json` — clean-room, no embeddings.
|
|
114
120
|
|
|
115
121
|
### Custom skills
|
|
@@ -144,6 +150,26 @@ relationships with `GraphQuery` — including **control inheritance** ("which
|
|
|
144
150
|
servers inherit physical controls from their enclave?"). Stdlib in-memory graph,
|
|
145
151
|
no Neo4j.
|
|
146
152
|
|
|
153
|
+
### STIG checklists (DISA `.ckl`/`.cklb`)
|
|
154
|
+
|
|
155
|
+
Take a raw DISA STIG benchmark all the way to a completed, eMASS/STIG-Viewer-
|
|
156
|
+
compatible checklist — entirely local (hostnames, IPs, and findings are CUI):
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
/stig new U_ASD_STIG_V6R1_Manual-xccdf.xml app.ckl # benchmark → blank .ckl
|
|
160
|
+
/graphrag build ./app # pull in the app's evidence
|
|
161
|
+
/loop 286 /stig-assess app.ckl # assess each rule vs evidence
|
|
162
|
+
/stig app.ckl open # list the open findings
|
|
163
|
+
/stig-remediate app.ckl SV-900010r1_rule # write an idempotent fix script
|
|
164
|
+
/stig graph app.ckl # ingest + auto-link rules → NIST controls
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`/stig new` parses the XCCDF benchmark (validated against the full 286-rule
|
|
168
|
+
Application STIG); `/stig-assess` reads your evidence and writes status +
|
|
169
|
+
finding-details back in place; `/stig graph` builds `STIG`/`STIG-Rule` nodes and
|
|
170
|
+
**auto-links each rule to its NIST 800-53 control** through DISA's CCI map
|
|
171
|
+
(`Control —SATISFIED_BY→ rule`), fetched once and cached offline.
|
|
172
|
+
|
|
147
173
|
## Install
|
|
148
174
|
|
|
149
175
|
```bash
|
|
@@ -173,10 +199,11 @@ commands to do the work, showing each as a collapsible tool card.
|
|
|
173
199
|
`◡ Keelhauling… (12s · ↓ 6.2k tokens · thinking with high effort)`
|
|
174
200
|
- Submit while it's working and the prompt **queues** (drains in order)
|
|
175
201
|
- Slash commands: `/model` · `/cwd` · `/undo` (revert last write) · `/back`
|
|
176
|
-
(rewind last turn) · `/status` · `/compact` (shrink context) · `/
|
|
177
|
-
(
|
|
178
|
-
|
|
179
|
-
`/
|
|
202
|
+
(rewind last turn) · `/status` · `/compact` (shrink context) · `/context`
|
|
203
|
+
(view/set the context-window budget) · `/graphrag` (build/query a knowledge
|
|
204
|
+
base) · `/skills` (list your `/<name>` skills) · `/loop` (repeat a prompt) ·
|
|
205
|
+
`/mcp` (list MCP servers) · `/rmf` & `/stig` (NIST 800-53 / DISA STIG
|
|
206
|
+
automation) · `/clear` · `/help` · `/quit`
|
|
180
207
|
|
|
181
208
|
It honors `AGENTS.md` / `DRYDOCK.md` in the working directory for project
|
|
182
209
|
conventions.
|
|
@@ -67,10 +67,14 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
|
|
|
67
67
|
| `/mcp` | List connected MCP servers + their tools |
|
|
68
68
|
| `/rmf bootstrap [families]` | Ingest the NIST SP 800-53 catalog (RMF automation) |
|
|
69
69
|
| `/rmf-control` · `/rmf-categorize` · `/rmf-review` · `/rmf-poam` | Bundled RMF skills |
|
|
70
|
+
| `/stig new <xccdf>` | Generate a blank `.ckl` from a DISA STIG benchmark |
|
|
71
|
+
| `/stig <ckl>` · `/stig <ckl> open` | Summarize a checklist · list findings by status |
|
|
72
|
+
| `/stig graph <ckl>` | Ingest a checklist into the RMF graph (auto-links rules→controls via CCI) |
|
|
73
|
+
| `/stig-assess <ckl>` · `/stig-remediate <ckl> <rule>` | Assess a rule vs evidence · write a fix script |
|
|
70
74
|
| `/model` · `/cwd` | Show/set model & endpoint · working directory |
|
|
71
75
|
| `/undo` · `/back` | Revert the last write · rewind the last turn |
|
|
72
|
-
| `/compact` · `/
|
|
73
|
-
| `/help` · `/quit` |
|
|
76
|
+
| `/compact` · `/context [n]` | Shrink context now · view/set the context-window budget |
|
|
77
|
+
| `/status` · `/clear` · `/help` · `/quit` | Session stats · reset · help · exit |
|
|
74
78
|
|
|
75
79
|
### Knowledge base (GraphRAG) — ingesting your documents
|
|
76
80
|
|
|
@@ -83,9 +87,11 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
|
|
|
83
87
|
|
|
84
88
|
Once built, the agent **automatically** retrieves from it (read-only `Knowledge`
|
|
85
89
|
tool) when a question touches your material. Ingests text formats
|
|
86
|
-
(`.md .txt .py .js .json .yaml .sql …`) **
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
(`.md .txt .py .js .json .yaml .sql …`), **PDF and Word (`.docx`)**, and **STIG
|
|
91
|
+
checklists (`.ckl`/`.cklb`)** — checklists are flattened to per-rule findings so
|
|
92
|
+
you can ask "which findings are open?". `.docx`/`.ckl` need nothing extra; PDF
|
|
93
|
+
uses the `pdftotext` binary (poppler) if present, else `pip install
|
|
94
|
+
drydock-cli[pdf]` (pypdf). The index is a single JSON at
|
|
89
95
|
`<project>/.drydock/graphrag.json` — clean-room, no embeddings.
|
|
90
96
|
|
|
91
97
|
### Custom skills
|
|
@@ -120,6 +126,26 @@ relationships with `GraphQuery` — including **control inheritance** ("which
|
|
|
120
126
|
servers inherit physical controls from their enclave?"). Stdlib in-memory graph,
|
|
121
127
|
no Neo4j.
|
|
122
128
|
|
|
129
|
+
### STIG checklists (DISA `.ckl`/`.cklb`)
|
|
130
|
+
|
|
131
|
+
Take a raw DISA STIG benchmark all the way to a completed, eMASS/STIG-Viewer-
|
|
132
|
+
compatible checklist — entirely local (hostnames, IPs, and findings are CUI):
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
/stig new U_ASD_STIG_V6R1_Manual-xccdf.xml app.ckl # benchmark → blank .ckl
|
|
136
|
+
/graphrag build ./app # pull in the app's evidence
|
|
137
|
+
/loop 286 /stig-assess app.ckl # assess each rule vs evidence
|
|
138
|
+
/stig app.ckl open # list the open findings
|
|
139
|
+
/stig-remediate app.ckl SV-900010r1_rule # write an idempotent fix script
|
|
140
|
+
/stig graph app.ckl # ingest + auto-link rules → NIST controls
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`/stig new` parses the XCCDF benchmark (validated against the full 286-rule
|
|
144
|
+
Application STIG); `/stig-assess` reads your evidence and writes status +
|
|
145
|
+
finding-details back in place; `/stig graph` builds `STIG`/`STIG-Rule` nodes and
|
|
146
|
+
**auto-links each rule to its NIST 800-53 control** through DISA's CCI map
|
|
147
|
+
(`Control —SATISFIED_BY→ rule`), fetched once and cached offline.
|
|
148
|
+
|
|
123
149
|
## Install
|
|
124
150
|
|
|
125
151
|
```bash
|
|
@@ -149,10 +175,11 @@ commands to do the work, showing each as a collapsible tool card.
|
|
|
149
175
|
`◡ Keelhauling… (12s · ↓ 6.2k tokens · thinking with high effort)`
|
|
150
176
|
- Submit while it's working and the prompt **queues** (drains in order)
|
|
151
177
|
- Slash commands: `/model` · `/cwd` · `/undo` (revert last write) · `/back`
|
|
152
|
-
(rewind last turn) · `/status` · `/compact` (shrink context) · `/
|
|
153
|
-
(
|
|
154
|
-
|
|
155
|
-
`/
|
|
178
|
+
(rewind last turn) · `/status` · `/compact` (shrink context) · `/context`
|
|
179
|
+
(view/set the context-window budget) · `/graphrag` (build/query a knowledge
|
|
180
|
+
base) · `/skills` (list your `/<name>` skills) · `/loop` (repeat a prompt) ·
|
|
181
|
+
`/mcp` (list MCP servers) · `/rmf` & `/stig` (NIST 800-53 / DISA STIG
|
|
182
|
+
automation) · `/clear` · `/help` · `/quit`
|
|
156
183
|
|
|
157
184
|
It honors `AGENTS.md` / `DRYDOCK.md` in the working directory for project
|
|
158
185
|
conventions.
|
|
@@ -56,6 +56,36 @@ PROVIDERS: dict[str, dict] = {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def probe_server_context(base_url: str, *, timeout: float = 4.0) -> int | None:
|
|
60
|
+
"""Best-effort: ask the model server its REAL context window (tokens).
|
|
61
|
+
llama.cpp exposes it on `/props` (n_ctx); vLLM puts `max_model_len` on
|
|
62
|
+
`/v1/models`. Returns None if unknown/unreachable — never raises. This is the
|
|
63
|
+
definitive diagnostic for a "stuck at N tokens" cap that isn't drydock's
|
|
64
|
+
config (it's the server's own `-c` / `--ctx-size` / `max_model_len`)."""
|
|
65
|
+
import json
|
|
66
|
+
import urllib.request
|
|
67
|
+
|
|
68
|
+
root = base_url.rstrip("/")
|
|
69
|
+
base_no_v1 = root[:-3].rstrip("/") if root.endswith("/v1") else root
|
|
70
|
+
|
|
71
|
+
def _n_ctx(d):
|
|
72
|
+
return (d.get("default_generation_settings") or {}).get("n_ctx") or d.get("n_ctx")
|
|
73
|
+
|
|
74
|
+
def _max_model_len(d):
|
|
75
|
+
return next((m.get("max_model_len") for m in d.get("data", []) if m.get("max_model_len")), None)
|
|
76
|
+
|
|
77
|
+
for url, pick in ((base_no_v1 + "/props", _n_ctx), (root + "/models", _max_model_len)):
|
|
78
|
+
try:
|
|
79
|
+
with urllib.request.urlopen(url, timeout=timeout) as r: # noqa: S310 (user's own server)
|
|
80
|
+
data = json.loads(r.read().decode("utf-8", "ignore"))
|
|
81
|
+
val = pick(data)
|
|
82
|
+
if isinstance(val, int) and val > 0:
|
|
83
|
+
return val
|
|
84
|
+
except Exception: # noqa: BLE001 — best-effort probe
|
|
85
|
+
continue
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
59
89
|
class LLMUnreachable(RuntimeError):
|
|
60
90
|
"""The configured LLM endpoint could not be reached. Carries a
|
|
61
91
|
user-facing message with remediation steps (shown verbatim in the TUI)."""
|
|
@@ -1017,15 +1017,34 @@ _SUBAGENT_SYSTEM = (
|
|
|
1017
1017
|
"tools: Read, Glob, Grep, and Bash (use Bash only to INSPECT — ls, cat, "
|
|
1018
1018
|
"grep, find, git log — never to modify files). Investigate the task you are "
|
|
1019
1019
|
"given, then STOP and reply with a concise, factual summary of what you "
|
|
1020
|
-
"found: concrete file:line references and the key code
|
|
1021
|
-
"
|
|
1020
|
+
"found: concrete file:line references and only the few key code snippets that "
|
|
1021
|
+
"matter, not narration and not whole files. Aim for under ~250 words — the "
|
|
1022
|
+
"main agent only gets this summary (not your tool output), so distill, don't "
|
|
1023
|
+
"dump. Do NOT try to edit or create files — the main agent acts on your findings."
|
|
1022
1024
|
)
|
|
1023
1025
|
|
|
1024
1026
|
|
|
1027
|
+
# A sub-agent's whole job is to keep its investigation OUT of the main agent's
|
|
1028
|
+
# context and hand back only a partition. The system prompt asks for a concise
|
|
1029
|
+
# summary, but a runaway model could still return a wall of text — so cap what
|
|
1030
|
+
# crosses back into the parent's window. ~4000 chars ≈ ~1000 tokens.
|
|
1031
|
+
_SUBAGENT_SUMMARY_CAP = 4000
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def _cap_summary(text: str) -> str:
|
|
1035
|
+
if len(text) <= _SUBAGENT_SUMMARY_CAP:
|
|
1036
|
+
return text
|
|
1037
|
+
head = text[:_SUBAGENT_SUMMARY_CAP].rsplit("\n", 1)[0] or text[:_SUBAGENT_SUMMARY_CAP]
|
|
1038
|
+
dropped = len(text) - len(head)
|
|
1039
|
+
return (head + f"\n[… sub-agent summary truncated, {dropped} chars dropped to keep it out "
|
|
1040
|
+
"of the main context. Ask a narrower follow-up sub-agent task if you need more.]")
|
|
1041
|
+
|
|
1042
|
+
|
|
1025
1043
|
def _run_subagent(prompt: str, config: dict) -> str:
|
|
1026
1044
|
"""Run one read-only sub-agent to completion and return its final summary.
|
|
1027
1045
|
Shared by `task` (one) and `Dispatch` (many in parallel). Hard-capped; never
|
|
1028
|
-
raises (a sub-agent must not crash the parent turn).
|
|
1046
|
+
raises (a sub-agent must not crash the parent turn). The returned summary is
|
|
1047
|
+
size-capped (_cap_summary) so a sub-agent can never bloat the main context."""
|
|
1029
1048
|
from drydock.agent import run as agent_run, AgentState, TurnDone
|
|
1030
1049
|
|
|
1031
1050
|
sub_state = AgentState()
|
|
@@ -1048,7 +1067,7 @@ def _run_subagent(prompt: str, config: dict) -> str:
|
|
|
1048
1067
|
return f"[sub-agent error: {e}]"
|
|
1049
1068
|
for msg in reversed(sub_state.messages):
|
|
1050
1069
|
if msg.get("role") == "assistant" and (msg.get("content") or "").strip():
|
|
1051
|
-
return msg["content"].strip()
|
|
1070
|
+
return _cap_summary(msg["content"].strip())
|
|
1052
1071
|
return f"[sub-agent finished {steps} step(s) with no summary]"
|
|
1053
1072
|
|
|
1054
1073
|
|
|
@@ -758,12 +758,12 @@ class DrydockApp(App):
|
|
|
758
758
|
arg = (arg or "").strip()
|
|
759
759
|
if not arg:
|
|
760
760
|
self._info(
|
|
761
|
-
f"context_limit: {limit:,} tokens (the '/{limit // 1024}k' in the ctx gauge).\n"
|
|
761
|
+
f"context_limit (drydock): {limit:,} tokens (the '/{limit // 1024}k' in the ctx gauge).\n"
|
|
762
762
|
"Source order: built-in default 65536 < ~/.drydock/config.toml < --context-limit.\n"
|
|
763
|
-
"
|
|
764
|
-
"
|
|
765
|
-
" /context <tokens> e.g. /context 65536 (saved to config.toml)"
|
|
763
|
+
"Probing your model server for its REAL context window…\n"
|
|
764
|
+
" To change drydock's budget: /context <tokens> (saved to config.toml)"
|
|
766
765
|
)
|
|
766
|
+
self.run_worker(lambda: self._probe_server_context(limit), thread=True)
|
|
767
767
|
return
|
|
768
768
|
try:
|
|
769
769
|
n = int(arg.replace(",", "").replace("k", "000").replace("K", "000"))
|
|
@@ -781,6 +781,30 @@ class DrydockApp(App):
|
|
|
781
781
|
"least this, or the server will still cap you below it."
|
|
782
782
|
)
|
|
783
783
|
|
|
784
|
+
def _probe_server_context(self, limit: int) -> None:
|
|
785
|
+
"""Worker: ask the model server its real context window and report whether
|
|
786
|
+
IT (not drydock's config) is the thing capping you — the definitive answer
|
|
787
|
+
to 'stuck at N tokens'."""
|
|
788
|
+
from drydock import providers
|
|
789
|
+
|
|
790
|
+
base_url = self.config.get("base_url") or providers.PROVIDERS.get(
|
|
791
|
+
self.config.get("provider") or "vllm", {}).get("base_url", "http://localhost:8000/v1")
|
|
792
|
+
n_ctx = providers.probe_server_context(base_url)
|
|
793
|
+
if n_ctx is None:
|
|
794
|
+
msg = (f"Model server ({base_url}) didn't report its context size "
|
|
795
|
+
"(not llama.cpp /props or vLLM max_model_len, or unreachable). "
|
|
796
|
+
f"Your effective cap is the smaller of drydock's {limit:,} and the "
|
|
797
|
+
"server's own -c/--ctx-size.")
|
|
798
|
+
elif n_ctx < limit:
|
|
799
|
+
msg = (f"⚠ Model server reports n_ctx = {n_ctx:,} tokens — SMALLER than "
|
|
800
|
+
f"drydock's {limit:,}. The SERVER is your real cap (you'll stick near "
|
|
801
|
+
f"{n_ctx // 1024}k). Restart it with a larger -c/--ctx-size/max_model_len, "
|
|
802
|
+
"or lower /context to match.")
|
|
803
|
+
else:
|
|
804
|
+
msg = (f"✓ Model server reports n_ctx = {n_ctx:,} tokens (≥ drydock's {limit:,}), "
|
|
805
|
+
"so drydock's budget is the effective limit — no server-side cap.")
|
|
806
|
+
self.call_from_thread(self._info, msg)
|
|
807
|
+
|
|
784
808
|
def _cmd_compact(self) -> None:
|
|
785
809
|
"""Manually compact the conversation to reclaim context NOW, without
|
|
786
810
|
waiting for the automatic 60%-of-window threshold (agent.maybe_compact).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: drydock-cli
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.76
|
|
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
|
|
@@ -91,10 +91,14 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
|
|
|
91
91
|
| `/mcp` | List connected MCP servers + their tools |
|
|
92
92
|
| `/rmf bootstrap [families]` | Ingest the NIST SP 800-53 catalog (RMF automation) |
|
|
93
93
|
| `/rmf-control` · `/rmf-categorize` · `/rmf-review` · `/rmf-poam` | Bundled RMF skills |
|
|
94
|
+
| `/stig new <xccdf>` | Generate a blank `.ckl` from a DISA STIG benchmark |
|
|
95
|
+
| `/stig <ckl>` · `/stig <ckl> open` | Summarize a checklist · list findings by status |
|
|
96
|
+
| `/stig graph <ckl>` | Ingest a checklist into the RMF graph (auto-links rules→controls via CCI) |
|
|
97
|
+
| `/stig-assess <ckl>` · `/stig-remediate <ckl> <rule>` | Assess a rule vs evidence · write a fix script |
|
|
94
98
|
| `/model` · `/cwd` | Show/set model & endpoint · working directory |
|
|
95
99
|
| `/undo` · `/back` | Revert the last write · rewind the last turn |
|
|
96
|
-
| `/compact` · `/
|
|
97
|
-
| `/help` · `/quit` |
|
|
100
|
+
| `/compact` · `/context [n]` | Shrink context now · view/set the context-window budget |
|
|
101
|
+
| `/status` · `/clear` · `/help` · `/quit` | Session stats · reset · help · exit |
|
|
98
102
|
|
|
99
103
|
### Knowledge base (GraphRAG) — ingesting your documents
|
|
100
104
|
|
|
@@ -107,9 +111,11 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
|
|
|
107
111
|
|
|
108
112
|
Once built, the agent **automatically** retrieves from it (read-only `Knowledge`
|
|
109
113
|
tool) when a question touches your material. Ingests text formats
|
|
110
|
-
(`.md .txt .py .js .json .yaml .sql …`) **
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
(`.md .txt .py .js .json .yaml .sql …`), **PDF and Word (`.docx`)**, and **STIG
|
|
115
|
+
checklists (`.ckl`/`.cklb`)** — checklists are flattened to per-rule findings so
|
|
116
|
+
you can ask "which findings are open?". `.docx`/`.ckl` need nothing extra; PDF
|
|
117
|
+
uses the `pdftotext` binary (poppler) if present, else `pip install
|
|
118
|
+
drydock-cli[pdf]` (pypdf). The index is a single JSON at
|
|
113
119
|
`<project>/.drydock/graphrag.json` — clean-room, no embeddings.
|
|
114
120
|
|
|
115
121
|
### Custom skills
|
|
@@ -144,6 +150,26 @@ relationships with `GraphQuery` — including **control inheritance** ("which
|
|
|
144
150
|
servers inherit physical controls from their enclave?"). Stdlib in-memory graph,
|
|
145
151
|
no Neo4j.
|
|
146
152
|
|
|
153
|
+
### STIG checklists (DISA `.ckl`/`.cklb`)
|
|
154
|
+
|
|
155
|
+
Take a raw DISA STIG benchmark all the way to a completed, eMASS/STIG-Viewer-
|
|
156
|
+
compatible checklist — entirely local (hostnames, IPs, and findings are CUI):
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
/stig new U_ASD_STIG_V6R1_Manual-xccdf.xml app.ckl # benchmark → blank .ckl
|
|
160
|
+
/graphrag build ./app # pull in the app's evidence
|
|
161
|
+
/loop 286 /stig-assess app.ckl # assess each rule vs evidence
|
|
162
|
+
/stig app.ckl open # list the open findings
|
|
163
|
+
/stig-remediate app.ckl SV-900010r1_rule # write an idempotent fix script
|
|
164
|
+
/stig graph app.ckl # ingest + auto-link rules → NIST controls
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`/stig new` parses the XCCDF benchmark (validated against the full 286-rule
|
|
168
|
+
Application STIG); `/stig-assess` reads your evidence and writes status +
|
|
169
|
+
finding-details back in place; `/stig graph` builds `STIG`/`STIG-Rule` nodes and
|
|
170
|
+
**auto-links each rule to its NIST 800-53 control** through DISA's CCI map
|
|
171
|
+
(`Control —SATISFIED_BY→ rule`), fetched once and cached offline.
|
|
172
|
+
|
|
147
173
|
## Install
|
|
148
174
|
|
|
149
175
|
```bash
|
|
@@ -173,10 +199,11 @@ commands to do the work, showing each as a collapsible tool card.
|
|
|
173
199
|
`◡ Keelhauling… (12s · ↓ 6.2k tokens · thinking with high effort)`
|
|
174
200
|
- Submit while it's working and the prompt **queues** (drains in order)
|
|
175
201
|
- Slash commands: `/model` · `/cwd` · `/undo` (revert last write) · `/back`
|
|
176
|
-
(rewind last turn) · `/status` · `/compact` (shrink context) · `/
|
|
177
|
-
(
|
|
178
|
-
|
|
179
|
-
`/
|
|
202
|
+
(rewind last turn) · `/status` · `/compact` (shrink context) · `/context`
|
|
203
|
+
(view/set the context-window budget) · `/graphrag` (build/query a knowledge
|
|
204
|
+
base) · `/skills` (list your `/<name>` skills) · `/loop` (repeat a prompt) ·
|
|
205
|
+
`/mcp` (list MCP servers) · `/rmf` & `/stig` (NIST 800-53 / DISA STIG
|
|
206
|
+
automation) · `/clear` · `/help` · `/quit`
|
|
180
207
|
|
|
181
208
|
It honors `AGENTS.md` / `DRYDOCK.md` in the working directory for project
|
|
182
209
|
conventions.
|
|
@@ -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.76"
|
|
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"
|
|
@@ -60,3 +60,14 @@ def test_task_still_works_via_shared_runner(monkeypatch):
|
|
|
60
60
|
monkeypatch.setattr(T, "_run_subagent", lambda p, c: f"summary:{p}")
|
|
61
61
|
assert T.tool_task({"prompt": "explore X"}, {}) == "summary:explore X"
|
|
62
62
|
assert "needs a `prompt`" in T.tool_task({}, {})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_subagent_summary_is_capped():
|
|
66
|
+
"""A sub-agent's return is size-capped so its investigation can never bloat
|
|
67
|
+
the main agent's context — only a bounded partition crosses back."""
|
|
68
|
+
from drydock.tools import _cap_summary, _SUBAGENT_SUMMARY_CAP
|
|
69
|
+
assert _cap_summary("brief finding") == "brief finding" # short passes through
|
|
70
|
+
big = "\n".join(f"line {i} with some detail" for i in range(2000))
|
|
71
|
+
out = _cap_summary(big)
|
|
72
|
+
assert len(out) < _SUBAGENT_SUMMARY_CAP + 200 # bounded
|
|
73
|
+
assert "truncated" in out and "main context" in out # explains the cut
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Server context-window probe — the definitive 'what's capping me' diagnostic."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from drydock import providers
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _Resp:
|
|
10
|
+
def __init__(self, body): self._b = json.dumps(body).encode()
|
|
11
|
+
def read(self): return self._b
|
|
12
|
+
def __enter__(self): return self
|
|
13
|
+
def __exit__(self, *a): return False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_probe_llamacpp_props_n_ctx(monkeypatch):
|
|
17
|
+
import urllib.request
|
|
18
|
+
def fake(url, timeout=0):
|
|
19
|
+
assert url.endswith("/props") # llama.cpp endpoint, /v1 stripped
|
|
20
|
+
return _Resp({"default_generation_settings": {"n_ctx": 32768}})
|
|
21
|
+
monkeypatch.setattr(urllib.request, "urlopen", fake)
|
|
22
|
+
assert providers.probe_server_context("http://localhost:8000/v1") == 32768
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_probe_vllm_max_model_len(monkeypatch):
|
|
26
|
+
import urllib.request
|
|
27
|
+
def fake(url, timeout=0):
|
|
28
|
+
if url.endswith("/props"):
|
|
29
|
+
raise OSError("no props on vLLM")
|
|
30
|
+
return _Resp({"data": [{"id": "m", "max_model_len": 65536}]})
|
|
31
|
+
monkeypatch.setattr(urllib.request, "urlopen", fake)
|
|
32
|
+
assert providers.probe_server_context("http://localhost:8000/v1") == 65536
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_probe_unreachable_is_none(monkeypatch):
|
|
36
|
+
import urllib.request
|
|
37
|
+
def boom(url, timeout=0): raise OSError("offline")
|
|
38
|
+
monkeypatch.setattr(urllib.request, "urlopen", boom)
|
|
39
|
+
assert providers.probe_server_context("http://localhost:9999/v1") is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|