@miller-tech/uap 1.61.1 → 1.61.3
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/self-harness/middleware/path-normalizer.d.ts +2 -9
- package/dist/self-harness/middleware/path-normalizer.d.ts.map +1 -1
- package/dist/self-harness/middleware/path-normalizer.js +11 -38
- package/dist/self-harness/middleware/path-normalizer.js.map +1 -1
- package/package.json +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +50 -6
- package/tools/agents/scripts/toolcall_path_normalizer.py +62 -76
- package/tools/agents/tests/test_finalize_suppression.py +178 -0
- package/tools/agents/tests/test_path_normalizer_hardened.py +144 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tests for the hardened (filesystem-verified, same-directory-only) tool-call
|
|
3
|
+
path normalizer.
|
|
4
|
+
|
|
5
|
+
The heuristic predecessor silently RELOCATED writes across projects/worktrees
|
|
6
|
+
(e.g. octopus_invaders/js/config.js -> octopus-invader/space-shooter/js/config.js),
|
|
7
|
+
turning a loud self-correcting failure into a silent wrong-write. These tests
|
|
8
|
+
pin the conservative contract: only a filename may be repaired, only inside a
|
|
9
|
+
directory that already exists on disk, only when exactly one real sibling
|
|
10
|
+
matches — and a wrong/garbled directory or any ambiguity is left untouched.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import os
|
|
15
|
+
import tempfile
|
|
16
|
+
import unittest
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _load():
|
|
21
|
+
p = Path(__file__).resolve().parents[1] / "scripts" / "toolcall_path_normalizer.py"
|
|
22
|
+
spec = importlib.util.spec_from_file_location("toolcall_path_normalizer", p)
|
|
23
|
+
m = importlib.util.module_from_spec(spec)
|
|
24
|
+
spec.loader.exec_module(m)
|
|
25
|
+
return m
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
norm = _load()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _touch(path):
|
|
32
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
33
|
+
with open(path, "w") as f:
|
|
34
|
+
f.write("x")
|
|
35
|
+
return path
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestHardenedNormalizer(unittest.TestCase):
|
|
39
|
+
def setUp(self):
|
|
40
|
+
self._tmp = tempfile.TemporaryDirectory()
|
|
41
|
+
self.root = self._tmp.name
|
|
42
|
+
|
|
43
|
+
def tearDown(self):
|
|
44
|
+
self._tmp.cleanup()
|
|
45
|
+
|
|
46
|
+
# --- the repairs it SHOULD make (filename only, same real dir) ---
|
|
47
|
+
|
|
48
|
+
def test_fixes_case_in_same_directory(self):
|
|
49
|
+
real = _touch(os.path.join(self.root, "proj", "config.js"))
|
|
50
|
+
proposed = os.path.join(self.root, "proj", "Config.js") # wrong case, doesn't exist
|
|
51
|
+
path, changed, _ = norm.normalize_tool_path(proposed)
|
|
52
|
+
self.assertTrue(changed)
|
|
53
|
+
self.assertEqual(path, real)
|
|
54
|
+
|
|
55
|
+
def test_fixes_dropped_extension_in_same_directory(self):
|
|
56
|
+
real = _touch(os.path.join(self.root, "proj", "config.js"))
|
|
57
|
+
proposed = os.path.join(self.root, "proj", "configjs") # squash-match
|
|
58
|
+
path, changed, _ = norm.normalize_tool_path(proposed)
|
|
59
|
+
self.assertTrue(changed)
|
|
60
|
+
self.assertEqual(path, real)
|
|
61
|
+
|
|
62
|
+
def test_trims_whitespace_on_real_path(self):
|
|
63
|
+
real = _touch(os.path.join(self.root, "proj", "a.js"))
|
|
64
|
+
path, changed, reason = norm.normalize_tool_path(f" {real} ")
|
|
65
|
+
self.assertTrue(changed)
|
|
66
|
+
self.assertEqual(path, real)
|
|
67
|
+
self.assertIn("trimmed", reason)
|
|
68
|
+
|
|
69
|
+
# --- the corruption it MUST NOT cause anymore ---
|
|
70
|
+
|
|
71
|
+
def test_does_not_relocate_to_a_different_directory(self):
|
|
72
|
+
# config.js exists only in projA; a (non-existent) write to projB/config.js
|
|
73
|
+
# must NOT be snapped into projA — the live octopus-style corruption.
|
|
74
|
+
_touch(os.path.join(self.root, "projA", "config.js"))
|
|
75
|
+
os.makedirs(os.path.join(self.root, "projB")) # real but has no config.js
|
|
76
|
+
proposed = os.path.join(self.root, "projB", "config.js")
|
|
77
|
+
path, changed, _ = norm.normalize_tool_path(proposed)
|
|
78
|
+
self.assertFalse(changed)
|
|
79
|
+
self.assertEqual(path, proposed)
|
|
80
|
+
|
|
81
|
+
def test_garbled_directory_is_never_guessed(self):
|
|
82
|
+
# The real dir is 'octopus-invader/space-shooter/js'; the model wrote to a
|
|
83
|
+
# DIFFERENT, non-existent dir 'octopus_invaders/js'. Must be a no-op.
|
|
84
|
+
_touch(os.path.join(self.root, "octopus-invader", "space-shooter", "js", "config.js"))
|
|
85
|
+
proposed = os.path.join(self.root, "octopus_invaders", "js", "config.js")
|
|
86
|
+
path, changed, _ = norm.normalize_tool_path(proposed)
|
|
87
|
+
self.assertFalse(changed)
|
|
88
|
+
self.assertEqual(path, proposed)
|
|
89
|
+
|
|
90
|
+
def test_punctuation_only_directory_difference_is_not_crossed(self):
|
|
91
|
+
# 's-space-shooter' vs 's space-shooter' are DIFFERENT real dirs; the old
|
|
92
|
+
# squash() guard treated them as the same. Must not relocate.
|
|
93
|
+
_touch(os.path.join(self.root, "s space-shooter", "css", "styles.css"))
|
|
94
|
+
os.makedirs(os.path.join(self.root, "s-space-shooter", "css"))
|
|
95
|
+
proposed = os.path.join(self.root, "s-space-shooter", "css", "styles.css")
|
|
96
|
+
path, changed, _ = norm.normalize_tool_path(proposed)
|
|
97
|
+
self.assertFalse(changed)
|
|
98
|
+
self.assertEqual(path, proposed)
|
|
99
|
+
|
|
100
|
+
def test_ambiguous_match_is_left_alone(self):
|
|
101
|
+
# Two files squash-match the garbled basename -> ambiguous -> no-op.
|
|
102
|
+
_touch(os.path.join(self.root, "proj", "config.js"))
|
|
103
|
+
_touch(os.path.join(self.root, "proj", "config.ts"))
|
|
104
|
+
proposed = os.path.join(self.root, "proj", "configjs?") # squashes to 'configjs'/'configts'?
|
|
105
|
+
# Make it genuinely ambiguous: 'config' squashes both 'config.js' and 'config.ts'.
|
|
106
|
+
proposed = os.path.join(self.root, "proj", "config")
|
|
107
|
+
path, changed, _ = norm.normalize_tool_path(proposed)
|
|
108
|
+
self.assertFalse(changed)
|
|
109
|
+
self.assertEqual(path, proposed)
|
|
110
|
+
|
|
111
|
+
def test_legitimate_new_file_passes_through(self):
|
|
112
|
+
# Creating a brand-new file in a real dir with no sibling match -> unchanged.
|
|
113
|
+
os.makedirs(os.path.join(self.root, "proj"))
|
|
114
|
+
proposed = os.path.join(self.root, "proj", "brand_new_helper.js")
|
|
115
|
+
path, changed, _ = norm.normalize_tool_path(proposed)
|
|
116
|
+
self.assertFalse(changed)
|
|
117
|
+
self.assertEqual(path, proposed)
|
|
118
|
+
|
|
119
|
+
def test_already_correct_path_unchanged(self):
|
|
120
|
+
real = _touch(os.path.join(self.root, "proj", "a.js"))
|
|
121
|
+
path, changed, _ = norm.normalize_tool_path(real)
|
|
122
|
+
self.assertFalse(changed)
|
|
123
|
+
self.assertEqual(path, real)
|
|
124
|
+
|
|
125
|
+
def test_relative_path_is_not_touched(self):
|
|
126
|
+
# No reliable cwd at the proxy -> relative paths pass through untouched.
|
|
127
|
+
path, changed, _ = norm.normalize_tool_path("src/Config.js")
|
|
128
|
+
self.assertFalse(changed)
|
|
129
|
+
self.assertEqual(path, "src/Config.js")
|
|
130
|
+
|
|
131
|
+
# --- integration through the public entry point ---
|
|
132
|
+
|
|
133
|
+
def test_normalize_tool_uses_applies_and_reports(self):
|
|
134
|
+
real = _touch(os.path.join(self.root, "proj", "config.js"))
|
|
135
|
+
wrong = os.path.join(self.root, "proj", "CONFIG.JS")
|
|
136
|
+
tool_uses = [{"type": "tool_use", "id": "t1", "input": {"file_path": wrong, "content": "y"}}]
|
|
137
|
+
corrections = norm.normalize_tool_uses(tool_uses, known_paths=[])
|
|
138
|
+
self.assertEqual(tool_uses[0]["input"]["file_path"], real)
|
|
139
|
+
self.assertEqual(tool_uses[0]["input"]["content"], "y") # non-path arg untouched
|
|
140
|
+
self.assertEqual(len(corrections), 1)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
unittest.main()
|