@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/bin/cli.js +11 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/sandbox.d.ts +18 -0
- package/dist/cli/sandbox.d.ts.map +1 -0
- package/dist/cli/sandbox.js +87 -0
- package/dist/cli/sandbox.js.map +1 -0
- package/package.json +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +65 -12
- package/tools/agents/scripts/toolcall_path_normalizer.py +162 -0
- package/tools/agents/tests/test_path_containment.py +149 -0
|
@@ -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()
|