@miller-tech/uap 1.118.0 → 1.118.2
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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli/tool-calls.d.ts.map +1 -1
- package/dist/cli/tool-calls.js +26 -122
- package/dist/cli/tool-calls.js.map +1 -1
- package/dist/cli/wizard-config.d.ts +0 -6
- package/dist/cli/wizard-config.d.ts.map +1 -1
- package/dist/cli/wizard-config.js +0 -23
- package/dist/cli/wizard-config.js.map +1 -1
- package/package.json +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +93 -11
- package/tools/agents/tests/test_anthropic_proxy_streaming.py +20 -11
- package/tools/agents/tests/test_cycle_break_exploration.py +149 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Unit tests for cycle-break tool narrowing preserving the exploration escape hatch.
|
|
3
|
+
|
|
4
|
+
Regression: the CYCLE BREAK narrowing removed every cycling tool by name (and the
|
|
5
|
+
whole read-only class if any cycling tool was read-only). Because Bash can be a
|
|
6
|
+
cycling tool, the agent lost the Bash tool entirely and could no longer run any
|
|
7
|
+
bash command to explore the filesystem. The narrowing must keep the open-ended
|
|
8
|
+
exploration escape hatch (Bash/WebFetch/Agent) available so the agent can make a
|
|
9
|
+
DIFFERENT move — the cycle *hint* handles "vary the command".
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import importlib.util
|
|
13
|
+
import unittest
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_proxy_module():
|
|
18
|
+
proxy_path = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
|
|
19
|
+
spec = importlib.util.spec_from_file_location("anthropic_proxy", proxy_path)
|
|
20
|
+
assert spec is not None and spec.loader is not None
|
|
21
|
+
module = importlib.util.module_from_spec(spec)
|
|
22
|
+
spec.loader.exec_module(module)
|
|
23
|
+
return module
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
proxy = _load_proxy_module()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _tools(*names):
|
|
30
|
+
return [{"function": {"name": n}} for n in names]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _names(tools):
|
|
34
|
+
return sorted(t["function"]["name"] for t in tools)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TestCycleBreakExploration(unittest.TestCase):
|
|
38
|
+
def test_cycling_bash_keeps_bash(self):
|
|
39
|
+
"""The core bug: cycling on Bash must NOT remove the Bash tool."""
|
|
40
|
+
tools = _tools("Bash", "Read", "Write")
|
|
41
|
+
narrowed, expanded = proxy._narrow_tools_for_cycle_break(tools, {"Bash"}, set())
|
|
42
|
+
self.assertIn("Bash", _names(narrowed))
|
|
43
|
+
self.assertFalse(expanded) # Bash is not read-only, so no class expansion
|
|
44
|
+
|
|
45
|
+
def test_read_only_cycle_expands_class_but_keeps_exploration(self):
|
|
46
|
+
"""Cycling a read-only tool drops the whole read-only class, yet the
|
|
47
|
+
exploration escape hatch (Bash/WebFetch/Agent) survives."""
|
|
48
|
+
tools = _tools("Read", "Grep", "Glob", "Bash", "WebFetch", "Agent", "Write")
|
|
49
|
+
narrowed, expanded = proxy._narrow_tools_for_cycle_break(tools, {"Grep"}, set())
|
|
50
|
+
self.assertTrue(expanded)
|
|
51
|
+
kept = _names(narrowed)
|
|
52
|
+
# read-only class dropped
|
|
53
|
+
for n in ("Read", "Grep", "Glob"):
|
|
54
|
+
self.assertNotIn(n, kept)
|
|
55
|
+
# exploration escape hatch + writes preserved
|
|
56
|
+
for n in ("Bash", "WebFetch", "Agent", "Write"):
|
|
57
|
+
self.assertIn(n, kept)
|
|
58
|
+
|
|
59
|
+
def test_session_ban_is_honored_even_for_exploration_tools(self):
|
|
60
|
+
"""An explicit session ban is a stronger, deliberate signal than cycling
|
|
61
|
+
and IS honored — even for an exploration tool. (Only the cycling path
|
|
62
|
+
preserves the escape hatch.)"""
|
|
63
|
+
tools = _tools("Bash", "Read", "Write")
|
|
64
|
+
narrowed, _ = proxy._narrow_tools_for_cycle_break(tools, set(), {"Bash"})
|
|
65
|
+
self.assertNotIn("Bash", _names(narrowed))
|
|
66
|
+
|
|
67
|
+
def test_cycling_bash_kept_even_when_another_tool_is_banned(self):
|
|
68
|
+
"""A cycling Bash is preserved even while an unrelated tool is banned."""
|
|
69
|
+
tools = _tools("Bash", "Read", "Task")
|
|
70
|
+
narrowed, _ = proxy._narrow_tools_for_cycle_break(tools, {"Bash"}, {"Task"})
|
|
71
|
+
kept = _names(narrowed)
|
|
72
|
+
self.assertIn("Bash", kept) # cycling exploration hatch preserved
|
|
73
|
+
self.assertNotIn("Task", kept) # explicit ban honored
|
|
74
|
+
|
|
75
|
+
def test_case_insensitive_matching(self):
|
|
76
|
+
"""Lowercase 'bash' in the cycling set must still match a 'Bash' tool
|
|
77
|
+
(and keep it), and 'read' must drop a 'Read' tool."""
|
|
78
|
+
tools = _tools("Bash", "Read")
|
|
79
|
+
narrowed, expanded = proxy._narrow_tools_for_cycle_break(
|
|
80
|
+
tools, {"bash", "read"}, set()
|
|
81
|
+
)
|
|
82
|
+
kept = _names(narrowed)
|
|
83
|
+
self.assertIn("Bash", kept) # exploration hatch kept despite cycling
|
|
84
|
+
self.assertNotIn("Read", kept) # read-only dropped
|
|
85
|
+
self.assertTrue(expanded)
|
|
86
|
+
|
|
87
|
+
def test_floor_keeps_last_action_tool_when_ban_would_strip_it(self):
|
|
88
|
+
"""Floor invariant: if honoring a ban would leave NO exploration tool and
|
|
89
|
+
NO write tool, keep the original set — a loop-breaker must never strand
|
|
90
|
+
the agent with no way to make a different move."""
|
|
91
|
+
tools = _tools("Bash", "Read") # Read is read-only, not an action path
|
|
92
|
+
narrowed, _ = proxy._narrow_tools_for_cycle_break(tools, set(), {"Bash"})
|
|
93
|
+
self.assertIn("Bash", _names(narrowed)) # floor restored the escape hatch
|
|
94
|
+
|
|
95
|
+
def test_floor_not_triggered_when_a_write_tool_survives(self):
|
|
96
|
+
"""The ban IS honored when another action path (a write tool) remains."""
|
|
97
|
+
tools = _tools("Bash", "Read", "Write")
|
|
98
|
+
narrowed, _ = proxy._narrow_tools_for_cycle_break(tools, set(), {"Bash"})
|
|
99
|
+
kept = _names(narrowed)
|
|
100
|
+
self.assertNotIn("Bash", kept) # honored: Write still gives an action path
|
|
101
|
+
self.assertIn("Write", kept)
|
|
102
|
+
|
|
103
|
+
def test_tool_class_taxonomy_is_pairwise_disjoint(self):
|
|
104
|
+
"""Regression insurance: the three tool-class sets must never overlap, or
|
|
105
|
+
convergence accounting / cycle-break scope would silently change."""
|
|
106
|
+
ro = {c.lower() for c in proxy._READ_ONLY_TOOL_CLASS}
|
|
107
|
+
wr = {c.lower() for c in proxy._WRITE_TOOL_CLASS}
|
|
108
|
+
ex = {c.lower() for c in proxy._EXPLORATION_ESCAPE_TOOLS}
|
|
109
|
+
self.assertEqual(ro & wr, set())
|
|
110
|
+
self.assertEqual(ro & ex, set())
|
|
111
|
+
self.assertEqual(wr & ex, set())
|
|
112
|
+
|
|
113
|
+
def test_non_exploration_non_readonly_tool_dropped(self):
|
|
114
|
+
"""A cycling tool that is neither read-only nor an exploration hatch is
|
|
115
|
+
dropped, and the read-only class is NOT expanded."""
|
|
116
|
+
tools = _tools("SomeTool", "Read", "Bash")
|
|
117
|
+
narrowed, expanded = proxy._narrow_tools_for_cycle_break(
|
|
118
|
+
tools, {"SomeTool"}, set()
|
|
119
|
+
)
|
|
120
|
+
kept = _names(narrowed)
|
|
121
|
+
self.assertNotIn("SomeTool", kept)
|
|
122
|
+
self.assertIn("Read", kept) # class not expanded, Read survives
|
|
123
|
+
self.assertIn("Bash", kept)
|
|
124
|
+
self.assertFalse(expanded)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestAutoBanExemption(unittest.TestCase):
|
|
128
|
+
"""The session-ban accumulator is cycling-derived (ban after N detections).
|
|
129
|
+
Exploration escape-hatch tools must never be auto-banned, or the 'cannot
|
|
130
|
+
explore the filesystem' bug reappears on the Nth cycle."""
|
|
131
|
+
|
|
132
|
+
def test_bash_never_auto_banned_even_far_past_threshold(self):
|
|
133
|
+
self.assertFalse(proxy._should_auto_ban("bash", cycle_count=99, ban_at=3))
|
|
134
|
+
self.assertFalse(proxy._should_auto_ban("Bash", cycle_count=3, ban_at=3))
|
|
135
|
+
|
|
136
|
+
def test_other_exploration_tools_never_auto_banned(self):
|
|
137
|
+
for name in ("webfetch", "WebSearch", "agent", "task"):
|
|
138
|
+
self.assertFalse(proxy._should_auto_ban(name, cycle_count=10, ban_at=3))
|
|
139
|
+
|
|
140
|
+
def test_non_exploration_tool_banned_at_threshold(self):
|
|
141
|
+
self.assertTrue(proxy._should_auto_ban("edit", cycle_count=3, ban_at=3))
|
|
142
|
+
self.assertTrue(proxy._should_auto_ban("read", cycle_count=3, ban_at=3))
|
|
143
|
+
|
|
144
|
+
def test_non_exploration_tool_not_banned_below_threshold(self):
|
|
145
|
+
self.assertFalse(proxy._should_auto_ban("edit", cycle_count=2, ban_at=3))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
unittest.main()
|