@miller-tech/uap 1.62.0 → 1.64.0

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.
@@ -15,11 +15,15 @@ TS reference at src/self-harness/middleware/path-normalizer.ts.
15
15
  See docs/design/SELF_HARNESS.md §4 (P2).
16
16
  """
17
17
 
18
+ import difflib
18
19
  import os
19
20
  import re
20
21
 
21
22
  _PATH_ARG_KEYS = ("file_path", "path", "filePath", "notebook_path")
22
23
 
24
+ # Absolute paths a tool call might target. Used to scan/rewrite Bash commands.
25
+ _ABS_PATH_RE = re.compile(r"/(?:home|root|Users|tmp|var|opt|srv|mnt)/[A-Za-z0-9._\-/]+")
26
+
23
27
 
24
28
  def _squash(s: str) -> str:
25
29
  return re.sub(r"[^a-z0-9]", "", s.lower())
@@ -123,6 +127,164 @@ def extract_known_paths(anthropic_messages) -> list:
123
127
  return known
124
128
 
125
129
 
130
+ def _fuzzy_eq(a: str, b: str) -> bool:
131
+ """Two path components are 'the same intent' if they squash-match or are very
132
+ close (handles octopus_invaders ~ octopus-invaders / octus_invaders / octpus_)."""
133
+ if not a or not b:
134
+ return False
135
+ if _squash(a) == _squash(b):
136
+ return True
137
+ return difflib.SequenceMatcher(None, a.lower(), b.lower()).ratio() >= 0.78
138
+
139
+
140
+ def derive_workdir(known_paths, hint_text: str = "") -> str:
141
+ """Best-effort session working directory, VALIDATED against disk: the deepest
142
+ absolute directory that exists on disk among the paths the model used
143
+ (known_paths) and any absolute paths in hint_text (request/tool-result text).
144
+ Garbled variants (e.g. /home/cogtec/...) don't exist on disk, so the real
145
+ workdir is recovered. Returns '' if none found.
146
+ """
147
+ _STOP = {"/", "/home", "/root", "/tmp", "/var", "/opt", "/srv", "/mnt", "/Users"}
148
+ cands: set[str] = set()
149
+ for p in known_paths or []:
150
+ if isinstance(p, str) and p.startswith("/"):
151
+ cands.add(p if os.path.isdir(p) else os.path.dirname(p))
152
+ if hint_text:
153
+ for m in _ABS_PATH_RE.findall(hint_text):
154
+ cands.add(m if os.path.isdir(m) else os.path.dirname(m))
155
+
156
+ existing: list[str] = []
157
+ for c in cands:
158
+ d = c
159
+ while d and d not in _STOP and not os.path.isdir(d):
160
+ d = os.path.dirname(d)
161
+ if d and d not in _STOP and os.path.isdir(d):
162
+ existing.append(d)
163
+ if not existing:
164
+ return ""
165
+ # Prefer the PROJECT ROOT (a dir with .git/.uap/package.json) over a deep
166
+ # subdir — anchoring containment on the root catches more garbles. Walk each
167
+ # existing candidate up to its nearest project-root ancestor.
168
+ _MARKERS = (".git", ".uap", ".uap.json", "package.json")
169
+ roots: set[str] = set()
170
+ for d in existing:
171
+ x = d
172
+ while x and x not in _STOP:
173
+ if any(os.path.exists(os.path.join(x, mk)) for mk in _MARKERS):
174
+ roots.add(x)
175
+ break
176
+ x = os.path.dirname(x)
177
+ pool = roots or set(existing)
178
+ # Deepest (most specific) among the chosen pool.
179
+ return max(pool, key=lambda d: (len(d.split("/")), len(d)))
180
+
181
+
182
+ def _fs_correct_suffix(workdir: str, suffix: str) -> str:
183
+ """Walk `suffix` under `workdir`, fuzzy-correcting each intermediate DIRECTORY
184
+ component to an existing on-disk sibling when the exact name is absent but a
185
+ single close match exists (space-shootr -> space-shooter once that dir
186
+ exists). The final component (the file being created) is left as-is."""
187
+ if not suffix:
188
+ return suffix
189
+ parts = [x for x in suffix.split("/") if x]
190
+ cur = workdir
191
+ out: list[str] = []
192
+ for i, comp in enumerate(parts):
193
+ nxt = os.path.join(cur, comp)
194
+ if i == len(parts) - 1 or os.path.exists(nxt):
195
+ out.append(comp)
196
+ cur = nxt
197
+ continue
198
+ try:
199
+ cands = [
200
+ e for e in os.listdir(cur)
201
+ if os.path.isdir(os.path.join(cur, e)) and _fuzzy_eq(e, comp)
202
+ ]
203
+ except OSError:
204
+ cands = []
205
+ chosen = cands[0] if len(cands) == 1 else comp
206
+ out.append(chosen)
207
+ cur = os.path.join(cur, chosen)
208
+ return "/".join(out)
209
+
210
+
211
+ def contain_to_workdir(path: str, workdir: str):
212
+ """Snap a garbled in-workdir path back onto `workdir`. Returns
213
+ (new_path, changed, reason). Two garble classes, both handled:
214
+
215
+ * mangled absolute PREFIX / workdir name (/home/cogtek -> /home/cogtec,
216
+ octopus_invaders -> octus_invaders) — anchored by a fuzzy match of the
217
+ workdir-name component;
218
+ * mangled SUBDIR name in the suffix (space-shooter -> space-shootr) —
219
+ fuzzy-corrected against the real directories on disk.
220
+
221
+ Only ever relocates INTO the workdir, and never touches a path that exists
222
+ elsewhere (the OS sandbox blocks a genuine out-of-workdir write). Safe
223
+ precisely because the sandbox contains any mis-snap to the workdir.
224
+ """
225
+ if not path or not workdir or not path.startswith("/"):
226
+ return path, False, None
227
+ wd = workdir.rstrip("/")
228
+
229
+ if path == wd or path.startswith(wd + "/"):
230
+ # Already inside: only fix garbled subdir names against disk.
231
+ suffix = path[len(wd):].lstrip("/")
232
+ reason = "corrected garbled subdir(s) under the workdir"
233
+ elif os.path.exists(path):
234
+ return path, False, None # a real path elsewhere — don't touch it
235
+ else:
236
+ # Garbled prefix/workdir-name: anchor on a fuzzy workdir-name match.
237
+ wd_name = wd.rsplit("/", 1)[-1]
238
+ parts = [x for x in path.split("/") if x]
239
+ anchor = next(
240
+ (i for i in range(len(parts) - 1, -1, -1) if _fuzzy_eq(parts[i], wd_name)),
241
+ None,
242
+ )
243
+ if anchor is None:
244
+ return path, False, None
245
+ suffix = "/".join(parts[anchor + 1:])
246
+ reason = f"contained garbled out-of-workdir path to '{wd_name}'"
247
+
248
+ corrected = _fs_correct_suffix(wd, suffix)
249
+ new = wd + ("/" + corrected if corrected else "")
250
+ if new != path:
251
+ return new, True, reason
252
+ return path, False, None
253
+
254
+
255
+ def contain_tool_uses(tool_uses, workdir: str):
256
+ """Contain garbled out-of-workdir paths to `workdir` — in Write/Edit path args
257
+ AND in Bash command tokens. Returns corrections [(id, key, from, to, reason)].
258
+ """
259
+ corrections = []
260
+ if not workdir:
261
+ return corrections
262
+ for tu in tool_uses:
263
+ if not isinstance(tu, dict) or tu.get("type") != "tool_use":
264
+ continue
265
+ inp = tu.get("input")
266
+ if not isinstance(inp, dict):
267
+ continue
268
+ tu_id = tu.get("id", "")
269
+ for key in _PATH_ARG_KEYS:
270
+ v = inp.get(key)
271
+ if isinstance(v, str):
272
+ nv, changed, reason = contain_to_workdir(v, workdir)
273
+ if changed:
274
+ inp[key] = nv
275
+ corrections.append((tu_id, key, v, nv, reason))
276
+ cmd = inp.get("command")
277
+ if isinstance(cmd, str) and "/" in cmd:
278
+ def _sub(m):
279
+ nv, changed, _ = contain_to_workdir(m.group(0), workdir)
280
+ return nv if changed else m.group(0)
281
+ new_cmd = _ABS_PATH_RE.sub(_sub, cmd)
282
+ if new_cmd != cmd:
283
+ inp["command"] = new_cmd
284
+ corrections.append((tu_id, "command", cmd, new_cmd, "contained garbled path(s) in bash command"))
285
+ return corrections
286
+
287
+
126
288
  def normalize_tool_uses(tool_uses, known_paths):
127
289
  """Normalize path args of a list of Anthropic tool_use blocks in place.
128
290
  Returns the list of corrections [(tool_use_id, key, from, to, reason)].
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for proxy-side path containment — recovering the workdir and snapping a
3
+ small quant's GARBLED absolute paths back onto it (the failure where it mangles
4
+ the prefix: /home/cogtek -> /home/cogtec, octopus_invaders -> octus_invaders).
5
+ Safe under the OS sandbox, which contains any mis-snap to the workdir.
6
+ """
7
+
8
+ import importlib.util
9
+ import os
10
+ import tempfile
11
+ import unittest
12
+ from pathlib import Path
13
+
14
+
15
+ def _load():
16
+ # this file: tools/agents/tests/ -> module: tools/agents/scripts/
17
+ p = Path(__file__).resolve().parents[1] / "scripts" / "toolcall_path_normalizer.py"
18
+ spec = importlib.util.spec_from_file_location("toolcall_path_normalizer", p)
19
+ m = importlib.util.module_from_spec(spec)
20
+ spec.loader.exec_module(m)
21
+ return m
22
+
23
+
24
+ N = _load()
25
+
26
+
27
+ def _tu(tid, **inp):
28
+ return {"type": "tool_use", "id": tid, "name": "Write", "input": inp}
29
+
30
+
31
+ class TestDeriveWorkdir(unittest.TestCase):
32
+ def setUp(self):
33
+ self._tmp = tempfile.TemporaryDirectory()
34
+ self.wd = os.path.join(self._tmp.name, "octopus_invaders")
35
+ os.makedirs(os.path.join(self.wd, "space-shooter"))
36
+ os.makedirs(os.path.join(self.wd, ".git")) # project-root marker
37
+
38
+ def tearDown(self):
39
+ self._tmp.cleanup()
40
+
41
+ def test_recovers_workdir_from_correct_and_garbled_known_paths(self):
42
+ known = [
43
+ os.path.join(self.wd, "space-shooter", "js", "game.js"), # correct (parent exists)
44
+ self.wd.replace("octopus_invaders", "octus_invaders") + "/x.js", # garble (no exist)
45
+ "/home/cogtec/dev/octopus_invaders/y.js", # garble (no exist)
46
+ ]
47
+ self.assertEqual(N.derive_workdir(known), self.wd)
48
+
49
+ def test_uses_hint_text_when_known_all_garbled(self):
50
+ known = ["/home/cogtec/dev/octus_invaders/a.js"] # all garbled
51
+ hint = f"running in {self.wd} now"
52
+ self.assertEqual(N.derive_workdir(known, hint), self.wd)
53
+
54
+ def test_returns_empty_when_nothing_exists(self):
55
+ self.assertEqual(N.derive_workdir(["/home/nope/x/y.js"]), "")
56
+
57
+
58
+ class TestContainToWorkdir(unittest.TestCase):
59
+ def setUp(self):
60
+ self._tmp = tempfile.TemporaryDirectory()
61
+ self.wd = os.path.join(self._tmp.name, "octopus_invaders")
62
+ os.makedirs(self.wd)
63
+
64
+ def tearDown(self):
65
+ self._tmp.cleanup()
66
+
67
+ def test_garbled_prefix_contained(self):
68
+ p = "/home/cogtec/dev/octopus_invaders/space-shooter/js/game.js"
69
+ new, changed, _ = N.contain_to_workdir(p, self.wd)
70
+ self.assertTrue(changed)
71
+ self.assertEqual(new, self.wd + "/space-shooter/js/game.js")
72
+
73
+ def test_garbled_workdir_name_contained(self):
74
+ for bad in ("octus_invaders", "octpus_invaders", "octopus-invaders", "octopus_invders"):
75
+ p = f"/home/cogtec/dev/{bad}/space-shooter/css/styles.css"
76
+ new, changed, _ = N.contain_to_workdir(p, self.wd)
77
+ self.assertTrue(changed, bad)
78
+ self.assertEqual(new, self.wd + "/space-shooter/css/styles.css", bad)
79
+
80
+ def test_already_inside_unchanged(self):
81
+ p = self.wd + "/space-shooter/js/game.js"
82
+ _, changed, _ = N.contain_to_workdir(p, self.wd)
83
+ self.assertFalse(changed)
84
+
85
+ def test_real_existing_path_elsewhere_left_alone(self):
86
+ other = os.path.join(self._tmp.name, "real_other")
87
+ os.makedirs(other)
88
+ open(os.path.join(other, "f.txt"), "w").write("x")
89
+ p = os.path.join(other, "f.txt")
90
+ _, changed, _ = N.contain_to_workdir(p, self.wd)
91
+ self.assertFalse(changed)
92
+
93
+ def test_unrelated_nonexistent_path_not_contained(self):
94
+ # No component fuzzy-matches the workdir name -> leave it (sandbox blocks).
95
+ p = "/etc/cron.d/totally_unrelated"
96
+ _, changed, _ = N.contain_to_workdir(p, self.wd)
97
+ self.assertFalse(changed)
98
+
99
+ def test_garbled_subdir_corrected_against_disk(self):
100
+ # space-shooter exists on disk; a write to garbled space-shootr is fixed.
101
+ os.makedirs(os.path.join(self.wd, "space-shooter", "js"))
102
+ p = self.wd + "/space-shootr/js/game.js" # in-workdir but garbled subdir
103
+ new, changed, _ = N.contain_to_workdir(p, self.wd)
104
+ self.assertTrue(changed)
105
+ self.assertEqual(new, self.wd + "/space-shooter/js/game.js")
106
+
107
+ def test_garbled_prefix_and_subdir_both_corrected(self):
108
+ os.makedirs(os.path.join(self.wd, "space-shooter", "css"))
109
+ p = "/home/cogtec/dev/octus_invaders/space-shootr/css/styles.css"
110
+ new, changed, _ = N.contain_to_workdir(p, self.wd)
111
+ self.assertTrue(changed)
112
+ self.assertEqual(new, self.wd + "/space-shooter/css/styles.css")
113
+
114
+ def test_new_subdir_left_when_no_disk_match(self):
115
+ # First write that legitimately creates a new dir -> not fuzzy-mangled.
116
+ p = self.wd + "/space-shooter/js/game.js"
117
+ new, changed, _ = N.contain_to_workdir(p, self.wd)
118
+ self.assertFalse(changed) # nothing to correct; left as-is
119
+ self.assertEqual(new, p)
120
+
121
+
122
+ class TestContainToolUses(unittest.TestCase):
123
+ def setUp(self):
124
+ self._tmp = tempfile.TemporaryDirectory()
125
+ self.wd = os.path.join(self._tmp.name, "octopus_invaders")
126
+ os.makedirs(self.wd)
127
+
128
+ def tearDown(self):
129
+ self._tmp.cleanup()
130
+
131
+ def test_contains_write_path_and_bash_command(self):
132
+ tus = [
133
+ _tu("t1", file_path="/home/cogtec/dev/octus_invaders/space-shooter/js/game.js", content="x"),
134
+ {"type": "tool_use", "id": "t2", "name": "Bash",
135
+ "input": {"command": "mkdir -p /home/cogtk/dev/octopus_invaders/space-shooter/css && echo done"}},
136
+ ]
137
+ corr = N.contain_tool_uses(tus, self.wd)
138
+ self.assertEqual(tus[0]["input"]["file_path"], self.wd + "/space-shooter/js/game.js")
139
+ self.assertIn(self.wd + "/space-shooter/css", tus[1]["input"]["command"])
140
+ self.assertTrue(tus[1]["input"]["command"].startswith("mkdir -p "))
141
+ self.assertEqual(len(corr), 2)
142
+
143
+ def test_noop_without_workdir(self):
144
+ tus = [_tu("t1", file_path="/home/cogtec/dev/octus_invaders/a.js")]
145
+ self.assertEqual(N.contain_tool_uses(tus, ""), [])
146
+
147
+
148
+ if __name__ == "__main__":
149
+ unittest.main()