@miller-tech/uap 1.183.1 → 1.183.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/package.json +2 -2
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/templates/hooks/__pycache__/deliver_autoroute.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 +73 -5
- package/tools/agents/tests/test_tool_narrowing_core.py +103 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miller-tech/uap",
|
|
3
|
-
"version": "1.183.
|
|
3
|
+
"version": "1.183.2",
|
|
4
4
|
"description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"start": "node dist/bin/cli.js",
|
|
22
22
|
"test": "vitest",
|
|
23
23
|
"test:ci": "vitest run",
|
|
24
|
-
"test:enforcers": "UAP_PROXY_ENV_AUTOLOAD=0 python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_schema_diff_gate tools.agents.tests.test_stream_telemetry tools.agents.tests.test_project_telemetry_events tools.agents.tests.test_workdir_scope_enforcer tools.agents.tests.test_path_containment tools.agents.tests.test_path_normalizer_hardened tools.agents.tests.test_sandbox_tool_strip tools.agents.tests.test_proxy_env_loader tools.agents.tests.test_stream_required_tool tools.agents.tests.test_tool_call_wrapper_profiles tools.agents.tests.test_tool_convert_cache tools.agents.tests.test_doubling_break tools.agents.tests.test_error_loop_ignores_correctives tools.agents.tests.test_attractor_detection tools.agents.tests.test_client_disconnect tools.agents.tests.test_confidence_escalation tools.agents.tests.test_coordination_ban tools.agents.tests.test_coordination_early_ban tools.agents.tests.test_cycle_break_exploration tools.agents.tests.test_deferral_break tools.agents.tests.test_deliver_autoroute tools.agents.tests.test_delivery_enforcement_all_langs tools.agents.tests.test_delivery_enforcement_exemptions tools.agents.tests.test_delivery_enforcement_filepath tools.agents.tests.test_delivery_enforcement_web_and_bash tools.agents.tests.test_disconnect_watcher tools.agents.tests.test_empty_maxtokens_recovery tools.agents.tests.test_empty_tool_loop_break tools.agents.tests.test_enforcer_escape_hatches tools.agents.tests.test_error_loop_break tools.agents.tests.test_finalize_suppression tools.agents.tests.test_malformed_unclosed_think tools.agents.tests.test_mandate_beats_recon tools.agents.tests.test_mandate_deliver tools.agents.tests.test_overflow_truncate_count_tokens tools.agents.tests.test_passthrough_oauth tools.agents.tests.test_project_telemetry tools.agents.tests.test_proxy_auth_headers tools.agents.tests.test_prune_preserve_force_write tools.agents.tests.test_recon_deliver_gate tools.agents.tests.test_session_admission tools.agents.tests.test_stream_heartbeat tools.agents.tests.test_stuck_break_reattach tools.agents.tests.test_turn_count_breaker_periodic tools.agents.tests.test_upstream_chokepoint tools.agents.tests.test_vision_passthrough tools.agents.tests.test_worktree_required tools.agents.tests.test_enforcer_suite_coverage tools.agents.tests.test_validate_plan_gate tools.agents.tests.test_validate_plan_inside_project tools.agents.tests.test_anthropic_proxy_streaming tools.agents.tests.test_delivery_enforcement_worktree tools.agents.tests.test_output_token_ceilings",
|
|
24
|
+
"test:enforcers": "UAP_PROXY_ENV_AUTOLOAD=0 python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_schema_diff_gate tools.agents.tests.test_stream_telemetry tools.agents.tests.test_project_telemetry_events tools.agents.tests.test_workdir_scope_enforcer tools.agents.tests.test_path_containment tools.agents.tests.test_path_normalizer_hardened tools.agents.tests.test_sandbox_tool_strip tools.agents.tests.test_proxy_env_loader tools.agents.tests.test_stream_required_tool tools.agents.tests.test_tool_call_wrapper_profiles tools.agents.tests.test_tool_convert_cache tools.agents.tests.test_doubling_break tools.agents.tests.test_error_loop_ignores_correctives tools.agents.tests.test_attractor_detection tools.agents.tests.test_client_disconnect tools.agents.tests.test_confidence_escalation tools.agents.tests.test_coordination_ban tools.agents.tests.test_coordination_early_ban tools.agents.tests.test_cycle_break_exploration tools.agents.tests.test_deferral_break tools.agents.tests.test_deliver_autoroute tools.agents.tests.test_delivery_enforcement_all_langs tools.agents.tests.test_delivery_enforcement_exemptions tools.agents.tests.test_delivery_enforcement_filepath tools.agents.tests.test_delivery_enforcement_web_and_bash tools.agents.tests.test_disconnect_watcher tools.agents.tests.test_empty_maxtokens_recovery tools.agents.tests.test_empty_tool_loop_break tools.agents.tests.test_enforcer_escape_hatches tools.agents.tests.test_error_loop_break tools.agents.tests.test_finalize_suppression tools.agents.tests.test_malformed_unclosed_think tools.agents.tests.test_mandate_beats_recon tools.agents.tests.test_mandate_deliver tools.agents.tests.test_overflow_truncate_count_tokens tools.agents.tests.test_passthrough_oauth tools.agents.tests.test_project_telemetry tools.agents.tests.test_proxy_auth_headers tools.agents.tests.test_prune_preserve_force_write tools.agents.tests.test_recon_deliver_gate tools.agents.tests.test_session_admission tools.agents.tests.test_stream_heartbeat tools.agents.tests.test_stuck_break_reattach tools.agents.tests.test_turn_count_breaker_periodic tools.agents.tests.test_upstream_chokepoint tools.agents.tests.test_vision_passthrough tools.agents.tests.test_worktree_required tools.agents.tests.test_enforcer_suite_coverage tools.agents.tests.test_validate_plan_gate tools.agents.tests.test_validate_plan_inside_project tools.agents.tests.test_anthropic_proxy_streaming tools.agents.tests.test_delivery_enforcement_worktree tools.agents.tests.test_output_token_ceilings tools.agents.tests.test_tool_narrowing_core",
|
|
25
25
|
"test:coverage": "vitest --coverage",
|
|
26
26
|
"bench": "vitest --config vitest.bench.config.ts",
|
|
27
27
|
"lint": "eslint src --ext .ts",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -5113,13 +5113,64 @@ def _tokenize_for_tool_ranking(text: str) -> set[str]:
|
|
|
5113
5113
|
|
|
5114
5114
|
|
|
5115
5115
|
# Core action tools a coding agent must always retain through narrowing — losing
|
|
5116
|
-
# any of these strands the agent (it can read/think but not act).
|
|
5117
|
-
#
|
|
5116
|
+
# any of these strands the agent (it can read/think but not act).
|
|
5117
|
+
#
|
|
5118
|
+
# This started as Claude Code's canonical names only, and that made the guard
|
|
5119
|
+
# a no-op for every client that names its tools differently. Hermes (2026-08-03)
|
|
5120
|
+
# sends terminal/execute_code/write_file/search_files/patch — ZERO overlap — so
|
|
5121
|
+
# narrowing cut 36 tools to 8, kept browser_type/clarify/cronjob and dropped
|
|
5122
|
+
# terminal AND write_file. The model then tried to run `ls -la` through
|
|
5123
|
+
# browser_type, failed on "no browser session" three times, CYCLE BREAK removed
|
|
5124
|
+
# browser_type leaving 6 tools, and it spent the rest of the session emitting
|
|
5125
|
+
# `clarify` calls that said "I've been stuck in a loop". That is the exact
|
|
5126
|
+
# failure the comment here already claimed to have fixed, recurring for a
|
|
5127
|
+
# client whose tools have other names.
|
|
5128
|
+
#
|
|
5129
|
+
# Kept as an exact-match fast path; _is_core_action_tool() below adds semantic
|
|
5130
|
+
# classification so an unknown client is covered without an entry here.
|
|
5118
5131
|
_CORE_TOOL_NAMES = frozenset({
|
|
5132
|
+
# Claude Code canonical
|
|
5119
5133
|
"read", "write", "edit", "multiedit", "notebookedit",
|
|
5120
5134
|
"bash", "glob", "grep", "ls", "applypatch", "apply_patch",
|
|
5135
|
+
# Common equivalents across other clients (hermes, Forge, opencode, …)
|
|
5136
|
+
"terminal", "shell", "run_command", "run_shell_command", "execute_command",
|
|
5137
|
+
"execute_code", "python", "run_python",
|
|
5138
|
+
"write_file", "create_file", "save_file", "new_file",
|
|
5139
|
+
"edit_file", "patch", "str_replace", "str_replace_editor", "replace_in_file",
|
|
5140
|
+
"read_file", "view_file", "open_file", "cat",
|
|
5141
|
+
"list_dir", "list_files", "search_files", "find_files", "file_search",
|
|
5121
5142
|
})
|
|
5122
5143
|
|
|
5144
|
+
# Phrases that identify a core ACTION capability from a tool's description, for
|
|
5145
|
+
# clients whose names we do not know. Deliberately require a file/command noun:
|
|
5146
|
+
# "type text into an element" (browser_type) must NOT read as a file write, and
|
|
5147
|
+
# it is better to under-match here than to pin a browser tool as core.
|
|
5148
|
+
_CORE_TOOL_DESC_PATTERNS = (
|
|
5149
|
+
r"\b(execute|run)\b[^.]{0,40}\b(shell|bash|command|script|code)\b",
|
|
5150
|
+
r"\bcommand[- ]line\b",
|
|
5151
|
+
r"\b(write|create|save)\b[^.]{0,40}\bfile\b",
|
|
5152
|
+
r"\b(edit|modify|patch|replace)\b[^.]{0,40}\b(file|code)\b",
|
|
5153
|
+
r"\bread\b[^.]{0,40}\b(file|contents)\b",
|
|
5154
|
+
r"\b(list|search|find)\b[^.]{0,40}\b(file|directory|directories)\b",
|
|
5155
|
+
)
|
|
5156
|
+
|
|
5157
|
+
|
|
5158
|
+
def _is_core_action_tool(name: str, description: str = "") -> bool:
|
|
5159
|
+
"""True when a tool is a core action tool that narrowing must never drop.
|
|
5160
|
+
|
|
5161
|
+
Exact name match first (cheap, and what most clients hit), then a semantic
|
|
5162
|
+
read of the description so a client we have never seen still keeps its
|
|
5163
|
+
ability to act. Losing shell/write is what strands an agent; everything else
|
|
5164
|
+
is recoverable.
|
|
5165
|
+
"""
|
|
5166
|
+
nm = (name or "").strip().lower()
|
|
5167
|
+
if nm in _CORE_TOOL_NAMES:
|
|
5168
|
+
return True
|
|
5169
|
+
desc = (description or "").strip().lower()
|
|
5170
|
+
if not desc:
|
|
5171
|
+
return False
|
|
5172
|
+
return any(re.search(p, desc) for p in _CORE_TOOL_DESC_PATTERNS)
|
|
5173
|
+
|
|
5123
5174
|
|
|
5124
5175
|
def _narrow_tools_for_request(
|
|
5125
5176
|
anthropic_body: dict, openai_tools: list[dict]
|
|
@@ -5187,10 +5238,27 @@ def _narrow_tools_for_request(
|
|
|
5187
5238
|
# otherwise keep an arbitrary first-N subset — observed dropping
|
|
5188
5239
|
# Write/Edit/Bash entirely and stalling the agent on meta-tools. `keep` thus
|
|
5189
5240
|
# acts as a soft floor: core tools are added on top of the top-scored set.
|
|
5241
|
+
core_ids = set()
|
|
5190
5242
|
for tool in openai_tools:
|
|
5191
|
-
|
|
5192
|
-
if
|
|
5193
|
-
|
|
5243
|
+
fn = tool.get("function", {}) or {}
|
|
5244
|
+
if _is_core_action_tool(fn.get("name", ""), fn.get("description", "")):
|
|
5245
|
+
core_ids.add(id(tool))
|
|
5246
|
+
|
|
5247
|
+
# FAIL-SAFE: if NOTHING in this client's surface reads as a core action tool,
|
|
5248
|
+
# we do not understand it well enough to prune it, and pruning it anyway is
|
|
5249
|
+
# exactly how hermes lost terminal + write_file and burned a session looping.
|
|
5250
|
+
# Ranking is lexical overlap with the user's text, which happily promotes
|
|
5251
|
+
# cronjob/clarify over the tools that do the work. Keep everything and say so
|
|
5252
|
+
# — an unnarrowed request costs prompt tokens; a stranded agent costs the run.
|
|
5253
|
+
if not core_ids:
|
|
5254
|
+
logger.info(
|
|
5255
|
+
"TOOL NARROWING: skipped — no core action tool recognised among %d tools "
|
|
5256
|
+
"(unknown client surface; not pruning blind)",
|
|
5257
|
+
len(openai_tools),
|
|
5258
|
+
)
|
|
5259
|
+
return openai_tools
|
|
5260
|
+
|
|
5261
|
+
selected |= core_ids
|
|
5194
5262
|
narrowed = [tool for tool in openai_tools if id(tool) in selected]
|
|
5195
5263
|
|
|
5196
5264
|
top_names = [t.get("function", {}).get("name", "") for t in narrowed[:4]]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Tool narrowing must never strand an agent by dropping its core action tools.
|
|
2
|
+
|
|
3
|
+
Regression (hermes, 2026-08-03): _CORE_TOOL_NAMES listed only Claude Code's
|
|
4
|
+
canonical names, so the always-retain guard was a no-op for any other client.
|
|
5
|
+
Hermes sends terminal/execute_code/write_file/search_files/patch — zero overlap.
|
|
6
|
+
Narrowing cut 36 tools to 8, kept browser_type/clarify/cronjob and dropped
|
|
7
|
+
terminal AND write_file. The model tried to run `ls -la` through browser_type,
|
|
8
|
+
failed "no browser session" three times, CYCLE BREAK then removed browser_type
|
|
9
|
+
leaving 6 tools, and the session ended in 12 `clarify` calls whose text was
|
|
10
|
+
"I've been stuck in a loop".
|
|
11
|
+
"""
|
|
12
|
+
import importlib.util
|
|
13
|
+
import os
|
|
14
|
+
import unittest
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
os.environ["PROXY_TOOL_NARROWING"] = "on"
|
|
18
|
+
proxy_path = Path(__file__).resolve().parents[3] / "tools" / "agents" / "scripts" / "anthropic_proxy.py"
|
|
19
|
+
spec = importlib.util.spec_from_file_location("anthropic_proxy", proxy_path)
|
|
20
|
+
ap = importlib.util.module_from_spec(spec)
|
|
21
|
+
spec.loader.exec_module(ap)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def tool(name, desc=""):
|
|
25
|
+
return {"type": "function", "function": {"name": name, "description": desc}}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def body(text):
|
|
29
|
+
return {"messages": [{"role": "user", "content": [{"type": "text", "text": text}]}]}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class IsCoreActionToolTest(unittest.TestCase):
|
|
33
|
+
def test_claude_code_names_are_core(self):
|
|
34
|
+
for n in ("Bash", "Write", "Edit", "Read", "Glob", "Grep"):
|
|
35
|
+
self.assertTrue(ap._is_core_action_tool(n, ""), n)
|
|
36
|
+
|
|
37
|
+
def test_other_clients_names_are_core(self):
|
|
38
|
+
# The whole point: these are hermes/Forge names, not Claude Code's.
|
|
39
|
+
for n in ("terminal", "execute_code", "write_file", "read_file",
|
|
40
|
+
"search_files", "patch", "str_replace", "run_command"):
|
|
41
|
+
self.assertTrue(ap._is_core_action_tool(n, ""), n)
|
|
42
|
+
|
|
43
|
+
def test_unknown_name_classified_from_description(self):
|
|
44
|
+
# A client we have never seen still keeps the ability to act.
|
|
45
|
+
self.assertTrue(ap._is_core_action_tool("zsh_exec", "Execute a shell command"))
|
|
46
|
+
self.assertTrue(ap._is_core_action_tool("blob_put", "Write content to a file"))
|
|
47
|
+
self.assertTrue(ap._is_core_action_tool("slurp", "Read the contents of a file"))
|
|
48
|
+
|
|
49
|
+
def test_non_action_tools_are_not_core(self):
|
|
50
|
+
# browser_type is the trap: its description says "type text", which must
|
|
51
|
+
# NOT read as a file write, or the cycling tool gets pinned as core.
|
|
52
|
+
self.assertFalse(ap._is_core_action_tool("browser_type", "Type text into an element on the page"))
|
|
53
|
+
self.assertFalse(ap._is_core_action_tool("clarify", "Ask the user a clarifying question"))
|
|
54
|
+
self.assertFalse(ap._is_core_action_tool("cronjob", "Schedule a recurring job"))
|
|
55
|
+
self.assertFalse(ap._is_core_action_tool("memory", "Store or recall a memory"))
|
|
56
|
+
self.assertFalse(ap._is_core_action_tool("browser_navigate", "Navigate the browser to a URL"))
|
|
57
|
+
|
|
58
|
+
def test_empty_description_unknown_name_is_not_core(self):
|
|
59
|
+
self.assertFalse(ap._is_core_action_tool("mystery", ""))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class NarrowingRetainsCoreToolsTest(unittest.TestCase):
|
|
63
|
+
def hermes_surface(self):
|
|
64
|
+
tools = [
|
|
65
|
+
tool("terminal", "Run a shell command on the host"),
|
|
66
|
+
tool("write_file", "Write content to a file at a path"),
|
|
67
|
+
tool("execute_code", "Execute Python code in a sandbox"),
|
|
68
|
+
tool("read_file", "Read the contents of a file"),
|
|
69
|
+
tool("clarify", "Ask the user a clarifying question"),
|
|
70
|
+
tool("browser_type", "Type text into an element on the page"),
|
|
71
|
+
tool("cronjob", "Schedule a recurring job"),
|
|
72
|
+
tool("memory", "Store or recall a memory"),
|
|
73
|
+
]
|
|
74
|
+
tools += [tool(f"misc_{i}", f"Miscellaneous capability {i}") for i in range(28)]
|
|
75
|
+
return tools
|
|
76
|
+
|
|
77
|
+
def test_terminal_and_write_file_survive_narrowing(self):
|
|
78
|
+
# The exact regression: a mission whose words share nothing with the
|
|
79
|
+
# tool names, so lexical ranking alone promotes meta-tools.
|
|
80
|
+
out = ap._narrow_tools_for_request(
|
|
81
|
+
body("Build a complete vanilla JS and Canvas space shooter game"),
|
|
82
|
+
self.hermes_surface(),
|
|
83
|
+
)
|
|
84
|
+
names = [t["function"]["name"] for t in out]
|
|
85
|
+
self.assertIn("terminal", names)
|
|
86
|
+
self.assertIn("write_file", names)
|
|
87
|
+
self.assertIn("execute_code", names)
|
|
88
|
+
self.assertLess(len(out), 36, "narrowing should still narrow")
|
|
89
|
+
|
|
90
|
+
def test_no_recognisable_core_tool_disables_narrowing(self):
|
|
91
|
+
# Fail-safe: a surface we cannot classify is one we must not prune.
|
|
92
|
+
opaque = [tool(f"opaque_{i}", f"Capability {i}") for i in range(30)]
|
|
93
|
+
out = ap._narrow_tools_for_request(body("do the thing"), opaque)
|
|
94
|
+
self.assertEqual(len(out), len(opaque))
|
|
95
|
+
|
|
96
|
+
def test_narrowing_still_prunes_when_core_tools_present(self):
|
|
97
|
+
# The fail-safe must not become a blanket opt-out.
|
|
98
|
+
out = ap._narrow_tools_for_request(body("build a game"), self.hermes_surface())
|
|
99
|
+
self.assertLess(len(out), 36)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
unittest.main()
|