drydock-cli 3.0.130__tar.gz → 3.0.133__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.130/drydock_cli.egg-info → drydock_cli-3.0.133}/PKG-INFO +1 -1
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/agent.py +15 -8
- drydock_cli-3.0.133/drydock/builtin_skills/nist-ai-rmf.md +28 -0
- drydock_cli-3.0.133/drydock/builtin_skills/nist-csf.md +27 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/tool_registry.py +21 -4
- drydock_cli-3.0.133/drydock/tool_result.py +101 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock_cli.egg-info/SOURCES.txt +4 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/pyproject.toml +1 -1
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_leaked_tool_call.py +5 -2
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_skills.py +1 -0
- drydock_cli-3.0.133/tests/test_tool_result.py +50 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/LICENSE +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/NOTICE +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/README.md +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/ml-data.md +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/ml-debug.md +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/ml-finetune.md +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/ml-metrics.md +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/ml-rl.md +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/ml-train.md +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/cci.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/cli.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/config.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/detect.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/events.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/extract.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/guards.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/poam.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/providers.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/recipes.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/skills.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/stig.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/suggest.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/task_state.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/verification.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/web.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/setup.cfg +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_config.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_events.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_recipes.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_repetition_interrupt.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_rolling_plan.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_suggest.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_task_state.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_verification_gate.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_xccdf.py +0 -0
|
@@ -34,7 +34,7 @@ def _plan_has_unfinished(config: dict) -> bool:
|
|
|
34
34
|
return bool(todo) and any(status != "done" for _, status in todo)
|
|
35
35
|
|
|
36
36
|
from drydock.providers import stream, AssistantTurn, ReasoningChunk, TextChunk, StallRetry, RepetitionDetected
|
|
37
|
-
from drydock.tool_registry import schemas,
|
|
37
|
+
from drydock.tool_registry import schemas, execute_structured
|
|
38
38
|
from drydock.tools import register_all
|
|
39
39
|
|
|
40
40
|
# Register the built-in tools as a side effect of importing the agent. This is
|
|
@@ -49,7 +49,7 @@ from drydock.compaction import (
|
|
|
49
49
|
from drydock.loop_detect import LoopTracker
|
|
50
50
|
from drydock.task_state import TaskState
|
|
51
51
|
from drydock.verification import looks_like_verification, parse_evidence
|
|
52
|
-
from drydock.events import emit as _emit
|
|
52
|
+
from drydock.events import EventLog, emit as _emit
|
|
53
53
|
from drydock.tuning import (
|
|
54
54
|
filter_tool_schemas,
|
|
55
55
|
hallucinated_tool_message,
|
|
@@ -84,7 +84,7 @@ class AgentState:
|
|
|
84
84
|
turn_count: int = 0
|
|
85
85
|
current_effort: str = "" # "high"/"low" of the in-flight LLM call (for the UI)
|
|
86
86
|
task: "TaskState" = field(default_factory=lambda: TaskState()) # structured objective
|
|
87
|
-
events:
|
|
87
|
+
events: "EventLog | None" = None # optional durable execution trace
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
def drop_last_turn(messages: list) -> bool:
|
|
@@ -422,14 +422,17 @@ def run(
|
|
|
422
422
|
halluc = hallucinated_tool_message(tc["name"])
|
|
423
423
|
if halluc is not None:
|
|
424
424
|
result = halluc
|
|
425
|
+
tool_result = None
|
|
425
426
|
elif allow is not None and tc["name"] not in allow:
|
|
426
427
|
result = (
|
|
427
428
|
f"[The '{tc['name']}' tool is not available here. You may use "
|
|
428
429
|
f"only: {', '.join(allow)}. Use one of those, or reply with "
|
|
429
430
|
"your final summary.]"
|
|
430
431
|
)
|
|
432
|
+
tool_result = None
|
|
431
433
|
else:
|
|
432
|
-
|
|
434
|
+
tool_result = execute_structured(tc["name"], tc["input"], config)
|
|
435
|
+
result = tool_result.text
|
|
433
436
|
# Track consecutive byte-identical calls — same name, args AND raw
|
|
434
437
|
# result (captured before annotate prepends its note, which changes
|
|
435
438
|
# each call) — for the safety valve below. A differing result
|
|
@@ -459,10 +462,14 @@ def run(
|
|
|
459
462
|
_emit(state, "plan", version=state.task.plan.version,
|
|
460
463
|
steps=[(s["id"], s["status"]) for s in state.task.plan.steps])
|
|
461
464
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
465
|
+
if tool_result is not None:
|
|
466
|
+
_emit(state, "tool", input=str(tc.get("input"))[:200],
|
|
467
|
+
**tool_result.to_event())
|
|
468
|
+
else:
|
|
469
|
+
_emit(state, "tool", name=tc["name"],
|
|
470
|
+
input=str(tc.get("input"))[:200],
|
|
471
|
+
result_chars=len(str(result)),
|
|
472
|
+
status="ok")
|
|
466
473
|
yield ToolEnd(tc["name"], result)
|
|
467
474
|
|
|
468
475
|
# Append tool result
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nist-ai-rmf
|
|
3
|
+
description: Apply the NIST AI Risk Management Framework (AI RMF 1.0) to govern, map, measure, and manage AI system risk
|
|
4
|
+
---
|
|
5
|
+
Help apply the NIST AI Risk Management Framework (AI RMF 1.0) to an AI system.
|
|
6
|
+
This is a GOVERNANCE and risk-management task — trustworthy, responsible AI. If the
|
|
7
|
+
user has not described the AI system, its purpose, data, and deployment context, ask
|
|
8
|
+
for those first. Then work through the four AI RMF functions:
|
|
9
|
+
|
|
10
|
+
1. GOVERN — the culture and processes for managing AI risk. Recommend policies,
|
|
11
|
+
roles/accountability, risk tolerance, and how AI risks are documented and escalated
|
|
12
|
+
across the AI lifecycle and the supply chain.
|
|
13
|
+
2. MAP — establish context and identify risks. Frame the system's purpose, stakeholders,
|
|
14
|
+
and potential impacts; enumerate risks to individuals, groups, and society (bias,
|
|
15
|
+
safety, privacy, security, misuse), and the assumptions/limitations.
|
|
16
|
+
3. MEASURE — analyze, assess, and track the identified risks with concrete methods and
|
|
17
|
+
metrics: evaluate the trustworthiness characteristics (valid & reliable, safe, secure
|
|
18
|
+
& resilient, accountable & transparent, explainable & interpretable, privacy-enhanced,
|
|
19
|
+
fair with harmful bias managed), and define test/monitoring procedures.
|
|
20
|
+
4. MANAGE — prioritize and act on risks: treatment decisions (mitigate/transfer/avoid/
|
|
21
|
+
accept), resource allocation, incident response, and ongoing monitoring with feedback
|
|
22
|
+
into GOVERN and MAP.
|
|
23
|
+
|
|
24
|
+
Be concrete: tie each recommendation to the relevant AI RMF function and the
|
|
25
|
+
trustworthiness characteristics, and produce actionable next steps (owners, artifacts,
|
|
26
|
+
review cadence). This is for building trustworthy AI — never for enabling misuse.
|
|
27
|
+
|
|
28
|
+
AI system / context: $ARGS
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nist-csf
|
|
3
|
+
description: Assess and improve cybersecurity posture with the NIST Cybersecurity Framework (CSF) 2.0
|
|
4
|
+
---
|
|
5
|
+
Help apply the NIST Cybersecurity Framework (CSF) 2.0 to an organization or system.
|
|
6
|
+
This is a DEFENSIVE governance and risk-management task. If the user hasn't described
|
|
7
|
+
the organization, its systems, and its current controls, ask first. Then work across
|
|
8
|
+
the six CSF 2.0 Functions:
|
|
9
|
+
|
|
10
|
+
1. GOVERN (GV) — cybersecurity strategy, roles & responsibilities, policy, risk
|
|
11
|
+
management strategy, and oversight of the supply chain.
|
|
12
|
+
2. IDENTIFY (ID) — asset management, business environment, risk assessment: know the
|
|
13
|
+
systems, data, and risks that must be protected.
|
|
14
|
+
3. PROTECT (PR) — safeguards: access control, awareness & training, data security,
|
|
15
|
+
platform security, and resilience of technology.
|
|
16
|
+
4. DETECT (DE) — continuous monitoring and adverse-event analysis to find incidents.
|
|
17
|
+
5. RESPOND (RS) — incident management, analysis, mitigation, and communication.
|
|
18
|
+
6. RECOVER (RC) — recovery planning and restoration of capabilities after an incident.
|
|
19
|
+
|
|
20
|
+
For an assessment, rate the organization against the CSF Implementation Tiers
|
|
21
|
+
(1 Partial → 2 Risk Informed → 3 Repeatable → 4 Adaptive) per Function, identify the
|
|
22
|
+
gap between the current and target profile, and produce a prioritized improvement
|
|
23
|
+
roadmap. Cite the relevant CSF Function/Category (e.g. GV.RM, ID.AM, PR.AA, DE.CM,
|
|
24
|
+
RS.MA, RC.RP). Be concrete and defensive — this supports protecting systems, not
|
|
25
|
+
attacking them.
|
|
26
|
+
|
|
27
|
+
Organization / system / scope: $ARGS
|
|
@@ -30,17 +30,34 @@ def schemas() -> list[dict]:
|
|
|
30
30
|
return [t.schema for t in _registry.values()]
|
|
31
31
|
|
|
32
32
|
def execute(name: str, params: dict, config: dict, max_output: int = 32000) -> str:
|
|
33
|
-
"""Execute a tool by name. Truncates large output.
|
|
33
|
+
"""Execute a tool by name. Truncates large output. Returns the text string
|
|
34
|
+
(backward-compatible); use execute_structured() for the ToolResult."""
|
|
35
|
+
return execute_structured(name, params, config, max_output).text
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def execute_structured(name: str, params: dict, config: dict, max_output: int = 32000):
|
|
39
|
+
"""Execute a tool and return a structured ToolResult (status/diagnostics/
|
|
40
|
+
retryable/duration + the text), classified from the tool's string output +
|
|
41
|
+
timing. The individual tools still return plain strings — this WRAPS them."""
|
|
42
|
+
import time as _time
|
|
43
|
+
|
|
44
|
+
from drydock.tool_result import ToolResult, classify
|
|
45
|
+
|
|
34
46
|
tool = get(name)
|
|
35
47
|
if tool is None:
|
|
36
|
-
return
|
|
48
|
+
return ToolResult(name=name, status="not_found", error_code="not_found",
|
|
49
|
+
text=f"Error: tool '{name}' not found.")
|
|
50
|
+
start = _time.monotonic()
|
|
37
51
|
try:
|
|
38
52
|
result = tool.func(params, config)
|
|
39
53
|
except Exception as e:
|
|
40
|
-
|
|
54
|
+
dur = int((_time.monotonic() - start) * 1000)
|
|
55
|
+
return ToolResult(name=name, status="error", error_code="exception",
|
|
56
|
+
text=f"Error executing {name}: {e}", duration_ms=dur)
|
|
57
|
+
dur = int((_time.monotonic() - start) * 1000)
|
|
41
58
|
|
|
42
59
|
if len(result) > max_output:
|
|
43
60
|
half = max_output // 2
|
|
44
61
|
quarter = max_output // 4
|
|
45
62
|
result = result[:half] + f"\n[... {len(result) - half - quarter} chars truncated ...]\n" + result[-quarter:]
|
|
46
|
-
return result
|
|
63
|
+
return classify(name, params, result, dur)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Structured tool results (Agent-Buildout PRD, section 7.4 / "structured tool
|
|
2
|
+
results"): a tool's output is not immediately reduced to a single text string —
|
|
3
|
+
status, diagnostics, retryability, and timing stay available to the controller for
|
|
4
|
+
loop/progress detection, verification, and recovery.
|
|
5
|
+
|
|
6
|
+
Incremental by design: tools still return plain strings (no rewrite), and
|
|
7
|
+
tool_registry.execute() still returns a string for full backward-compat. The new
|
|
8
|
+
execute_structured() WRAPS that string into a ToolResult, classifying status/error/
|
|
9
|
+
retryability from the text + timing. Logic original to Drydock.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ToolResult:
|
|
19
|
+
"""A tool call's outcome with structure the controller can reason about."""
|
|
20
|
+
name: str
|
|
21
|
+
status: str = "ok" # "ok" | "error" | "not_found"
|
|
22
|
+
text: str = "" # the human/model-facing string (backward-compat)
|
|
23
|
+
changed_state: bool = False # did it mutate files/env (Write/Edit/mutating Bash)?
|
|
24
|
+
retryable: bool = False # transient failure worth retrying?
|
|
25
|
+
error_code: str | None = None # coarse classification when status == error
|
|
26
|
+
exit_code: int | None = None # for Bash, if present
|
|
27
|
+
duration_ms: int = 0
|
|
28
|
+
diagnostics: list[str] = field(default_factory=list)
|
|
29
|
+
|
|
30
|
+
def to_event(self) -> dict:
|
|
31
|
+
"""Compact dict for the event log (no big blobs)."""
|
|
32
|
+
return {
|
|
33
|
+
"name": self.name, "status": self.status,
|
|
34
|
+
"changed_state": self.changed_state, "retryable": self.retryable,
|
|
35
|
+
"error_code": self.error_code, "exit_code": self.exit_code,
|
|
36
|
+
"duration_ms": self.duration_ms, "result_chars": len(self.text),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_EXIT = re.compile(r"\[exit code:\s*(-?\d+)\]")
|
|
41
|
+
_MUTATING_BASH = re.compile(
|
|
42
|
+
r"(?:^|[\s;&|])(?:rm|mv|cp|mkdir|touch|tee|sed\s+-i|dd|chmod|chown|ln|"
|
|
43
|
+
r"git\s+(?:commit|add|apply|checkout|reset|rm|mv)|pip\s+install|npm\s+i|"
|
|
44
|
+
r"apt|make(?:\s|$)|cargo\s+build|>|>>)\b", re.I,
|
|
45
|
+
)
|
|
46
|
+
# Error phrases that usually mean "try again might work" (transient).
|
|
47
|
+
_RETRYABLE = re.compile(
|
|
48
|
+
r"\b(timed out|timeout|connection (refused|reset|error)|temporarily|"
|
|
49
|
+
r"try again|rate limit|EAGAIN|ETIMEDOUT|network is unreachable|"
|
|
50
|
+
r"resource temporarily unavailable|503|502|429)\b", re.I,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _classify_error(text: str) -> tuple[str, bool]:
|
|
55
|
+
"""(error_code, retryable) for an error result string."""
|
|
56
|
+
t = text.lower()
|
|
57
|
+
if _RETRYABLE.search(text):
|
|
58
|
+
return "transient", True
|
|
59
|
+
if "not found" in t or "no such file" in t or "command not found" in t:
|
|
60
|
+
return "not_found", False
|
|
61
|
+
if "permission denied" in t:
|
|
62
|
+
return "permission", False
|
|
63
|
+
if "syntax error" in t or "invalid" in t:
|
|
64
|
+
return "invalid_input", False
|
|
65
|
+
if "timed out" in t:
|
|
66
|
+
return "timeout", True
|
|
67
|
+
return "error", False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def classify(name: str, params: dict, text: str, duration_ms: int) -> ToolResult:
|
|
71
|
+
"""Build a ToolResult from a tool's (name, params, string result). Never raises."""
|
|
72
|
+
text = text if isinstance(text, str) else str(text or "")
|
|
73
|
+
r = ToolResult(name=name, text=text, duration_ms=max(0, int(duration_ms)))
|
|
74
|
+
|
|
75
|
+
m = _EXIT.search(text)
|
|
76
|
+
if m:
|
|
77
|
+
try:
|
|
78
|
+
r.exit_code = int(m.group(1))
|
|
79
|
+
except ValueError:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
# Status: an "Error…" prefixed string, a "tool not found", or a non-zero Bash
|
|
83
|
+
# exit means the call did not succeed. Otherwise ok.
|
|
84
|
+
lowered = text.lstrip()[:60].lower()
|
|
85
|
+
if lowered.startswith("error: tool") and "not found" in text.lower():
|
|
86
|
+
r.status, r.error_code = "not_found", "not_found"
|
|
87
|
+
elif lowered.startswith("error"):
|
|
88
|
+
r.status = "error"
|
|
89
|
+
r.error_code, r.retryable = _classify_error(text)
|
|
90
|
+
elif r.exit_code not in (None, 0):
|
|
91
|
+
r.status = "error"
|
|
92
|
+
r.error_code, r.retryable = _classify_error(text)
|
|
93
|
+
|
|
94
|
+
# State change: Write/Edit always mutate; Bash only for mutating commands.
|
|
95
|
+
if name in ("Write", "Edit", "GitCommit", "BuildKnowledge", "GraphAdd"):
|
|
96
|
+
r.changed_state = r.status == "ok"
|
|
97
|
+
elif name == "Bash":
|
|
98
|
+
cmd = (params or {}).get("command", "") if isinstance(params, dict) else ""
|
|
99
|
+
r.changed_state = bool(_MUTATING_BASH.search(cmd)) and r.status == "ok"
|
|
100
|
+
|
|
101
|
+
return r
|
|
@@ -29,6 +29,7 @@ drydock/stig.py
|
|
|
29
29
|
drydock/suggest.py
|
|
30
30
|
drydock/task_state.py
|
|
31
31
|
drydock/tool_registry.py
|
|
32
|
+
drydock/tool_result.py
|
|
32
33
|
drydock/tuning.py
|
|
33
34
|
drydock/verification.py
|
|
34
35
|
drydock/web.py
|
|
@@ -39,6 +40,8 @@ drydock/builtin_skills/ml-finetune.md
|
|
|
39
40
|
drydock/builtin_skills/ml-metrics.md
|
|
40
41
|
drydock/builtin_skills/ml-rl.md
|
|
41
42
|
drydock/builtin_skills/ml-train.md
|
|
43
|
+
drydock/builtin_skills/nist-ai-rmf.md
|
|
44
|
+
drydock/builtin_skills/nist-csf.md
|
|
42
45
|
drydock/builtin_skills/rmf-categorize.md
|
|
43
46
|
drydock/builtin_skills/rmf-control.md
|
|
44
47
|
drydock/builtin_skills/rmf-poam.md
|
|
@@ -127,6 +130,7 @@ tests/test_todo.py
|
|
|
127
130
|
tests/test_tool_arg_coercion.py
|
|
128
131
|
tests/test_tool_arg_coercion_more.py
|
|
129
132
|
tests/test_tool_arg_parsing.py
|
|
133
|
+
tests/test_tool_result.py
|
|
130
134
|
tests/test_tools_undo.py
|
|
131
135
|
tests/test_tui.py
|
|
132
136
|
tests/test_tuning.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.133"
|
|
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"
|
|
@@ -83,8 +83,11 @@ def test_adaptive_reasoning_high_to_plan_then_low(monkeypatch):
|
|
|
83
83
|
yield AssistantTurn("done", [], 1, 1)
|
|
84
84
|
|
|
85
85
|
monkeypatch.setattr(agent_mod, "stream", fake_stream)
|
|
86
|
-
# execute
|
|
87
|
-
|
|
86
|
+
# execute would run a real Bash; stub it to keep the test hermetic. The loop
|
|
87
|
+
# now calls execute_structured (returns a ToolResult with .text).
|
|
88
|
+
from drydock.tool_result import ToolResult
|
|
89
|
+
monkeypatch.setattr(agent_mod, "execute_structured",
|
|
90
|
+
lambda name, inp, cfg: ToolResult(name=name, text="ok"))
|
|
88
91
|
st = AgentState()
|
|
89
92
|
list(run("go", st, {"model": "gemma4"}, "sys"))
|
|
90
93
|
# First (planning) turn high, the continuation that consumes the tool result low.
|
|
@@ -59,6 +59,7 @@ def test_no_user_or_project_skills_yields_only_builtins(tmp_path):
|
|
|
59
59
|
"rmf-control", "rmf-categorize", "rmf-review", "rmf-poam",
|
|
60
60
|
"stig-assess", "stig-remediate",
|
|
61
61
|
"ml-train", "ml-metrics", "ml-finetune", "ml-debug", "ml-rl", "ml-data",
|
|
62
|
+
"nist-ai-rmf", "nist-csf",
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Structured tool results (Agent-Buildout PRD 7.4): execute_structured() wraps a
|
|
2
|
+
tool's string output in a ToolResult (status/error/retryable/changed_state/duration)
|
|
3
|
+
while execute() stays a plain string (backward compatibility)."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from drydock.tool_result import ToolResult, classify
|
|
7
|
+
from drydock.tools import register_all
|
|
8
|
+
from drydock import tool_registry
|
|
9
|
+
|
|
10
|
+
register_all()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_execute_still_returns_str():
|
|
14
|
+
r = tool_registry.execute("Bash", {"command": "echo hi", "timeout": 5}, {"cwd": "/tmp"})
|
|
15
|
+
assert isinstance(r, str) and "hi" in r
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_structured_success():
|
|
19
|
+
r = tool_registry.execute_structured("Bash", {"command": "echo hi", "timeout": 5}, {"cwd": "/tmp"})
|
|
20
|
+
assert isinstance(r, ToolResult) and r.status == "ok" and r.duration_ms >= 0 and not r.changed_state
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_structured_failure_exit_code():
|
|
24
|
+
r = tool_registry.execute_structured(
|
|
25
|
+
"Bash", {"command": "python3 -c 'import sys;sys.exit(3)'", "timeout": 5}, {"cwd": "/tmp"})
|
|
26
|
+
assert r.status == "error" and r.exit_code == 3
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_not_found():
|
|
30
|
+
r = tool_registry.execute_structured("NoSuchTool", {}, {})
|
|
31
|
+
assert r.status == "not_found" and r.error_code == "not_found"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_classify_changed_state_and_retryable():
|
|
35
|
+
assert classify("Write", {}, "wrote 10 bytes", 1).changed_state
|
|
36
|
+
assert not classify("Read", {}, "contents", 1).changed_state
|
|
37
|
+
assert classify("Bash", {"command": "touch x"}, "", 1).changed_state
|
|
38
|
+
assert not classify("Bash", {"command": "ls -la"}, "files", 1).changed_state
|
|
39
|
+
r = classify("Bash", {"command": "curl x"}, "Error: connection refused", 1)
|
|
40
|
+
assert r.status == "error" and r.retryable and r.error_code == "transient"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_error_prefix_string():
|
|
44
|
+
r = classify("Edit", {}, "Error: old_string not found", 1)
|
|
45
|
+
assert r.status == "error" and not r.changed_state # failed edit didn't mutate
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_to_event_is_compact():
|
|
49
|
+
ev = classify("Bash", {"command": "echo x"}, "x", 5).to_event()
|
|
50
|
+
assert ev["status"] == "ok" and "result_chars" in ev and "text" not in ev
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|