@miller-tech/uap 1.83.0 → 1.84.1
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 +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/delivery_enforcement.py +20 -14
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +25 -0
- package/tools/agents/tests/test_delivery_enforcement_worktree.py +29 -0
- package/tools/agents/tests/test_tool_convert_cache.py +49 -0
package/package.json
CHANGED
|
Binary file
|
|
@@ -65,18 +65,21 @@ def _is_local_model_session() -> bool:
|
|
|
65
65
|
return any(h in base for h in ("127.0.0.1", "localhost", "0.0.0.0", "::1"))
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
# #2: a
|
|
69
|
-
# deadlocks under block
|
|
70
|
-
# live).
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
"",
|
|
79
|
-
}
|
|
68
|
+
# #2/#3a: how a LOCAL-model session is handled under block mode. A plain local
|
|
69
|
+
# session deadlocks under strict block (can read, never write -> recon loop,
|
|
70
|
+
# observed live). UAP_DELIVER_LOCAL_MODE selects the resolution:
|
|
71
|
+
# advisory (default) -> allow direct writes (fast; the proxy guards the model)
|
|
72
|
+
# deliver -> keep the block + route:deliver so the change is driven
|
|
73
|
+
# through `uap deliver` (VERIFIED path; pairs with
|
|
74
|
+
# UAP_DELIVER_AUTOROUTE)
|
|
75
|
+
# block -> strict block (no relaxation)
|
|
76
|
+
# Back-compat: UAP_DELIVER_LOCAL_ADVISORY=0 maps to "block".
|
|
77
|
+
def _local_mode() -> str:
|
|
78
|
+
m = os.environ.get("UAP_DELIVER_LOCAL_MODE", "").lower()
|
|
79
|
+
if m in {"advisory", "deliver", "block"}:
|
|
80
|
+
return m
|
|
81
|
+
adv = os.environ.get("UAP_DELIVER_LOCAL_ADVISORY", "on").lower()
|
|
82
|
+
return "advisory" if adv not in {"0", "off", "false", "no", ""} else "block"
|
|
80
83
|
|
|
81
84
|
|
|
82
85
|
def main() -> None:
|
|
@@ -124,8 +127,11 @@ def main() -> None:
|
|
|
124
127
|
)
|
|
125
128
|
|
|
126
129
|
mode = os.environ.get("UAP_ENFORCE_DELIVERY", "block").lower()
|
|
127
|
-
if mode == "block" and
|
|
128
|
-
|
|
130
|
+
if mode == "block" and _is_local_model_session():
|
|
131
|
+
# #3a: route-through-deliver keeps the block (route:deliver -> autoroute
|
|
132
|
+
# drives the verified deliver loop); advisory unblocks direct writes.
|
|
133
|
+
if _local_mode() == "advisory":
|
|
134
|
+
mode = "advisory"
|
|
129
135
|
if mode == "block":
|
|
130
136
|
# R1: emit a machine-actionable routing signal so a capable harness can
|
|
131
137
|
# auto-route the blocked change INTO `uap deliver` instead of leaving the
|
|
Binary file
|
|
@@ -3351,7 +3351,28 @@ def anthropic_to_openai_response(anthropic_resp: dict) -> dict:
|
|
|
3351
3351
|
}
|
|
3352
3352
|
|
|
3353
3353
|
|
|
3354
|
+
# A3: the anthropic->openai tool conversion + schema sanitize walks every tool's
|
|
3355
|
+
# (often deeply nested) JSON schema. The tool set is IDENTICAL across every turn
|
|
3356
|
+
# of a session, so this recomputed the same result each turn (observed: ~1
|
|
3357
|
+
# SCHEMA SANITIZE log per turn). Cache by a stable hash of the tool definitions.
|
|
3358
|
+
# Downstream only READS the converted dicts and FILTERS the list (narrowing), so
|
|
3359
|
+
# returning the cached object directly is safe.
|
|
3360
|
+
_TOOL_CONVERT_CACHE: "OrderedDict[str, list]" = OrderedDict()
|
|
3361
|
+
_TOOL_CONVERT_CACHE_MAX = 32
|
|
3362
|
+
|
|
3363
|
+
|
|
3354
3364
|
def _convert_anthropic_tools_to_openai(anthropic_tools: list[dict]) -> list[dict]:
|
|
3365
|
+
cache_key = None
|
|
3366
|
+
try:
|
|
3367
|
+
cache_key = hashlib.sha1(
|
|
3368
|
+
json.dumps(anthropic_tools, sort_keys=True, default=str).encode("utf-8")
|
|
3369
|
+
).hexdigest()
|
|
3370
|
+
except Exception:
|
|
3371
|
+
cache_key = None
|
|
3372
|
+
if cache_key is not None and cache_key in _TOOL_CONVERT_CACHE:
|
|
3373
|
+
_TOOL_CONVERT_CACHE.move_to_end(cache_key)
|
|
3374
|
+
return _TOOL_CONVERT_CACHE[cache_key]
|
|
3375
|
+
|
|
3355
3376
|
converted = []
|
|
3356
3377
|
removed_pattern_fields = 0
|
|
3357
3378
|
for tool in anthropic_tools:
|
|
@@ -3375,6 +3396,10 @@ def _convert_anthropic_tools_to_openai(anthropic_tools: list[dict]) -> list[dict
|
|
|
3375
3396
|
removed_pattern_fields,
|
|
3376
3397
|
len(anthropic_tools),
|
|
3377
3398
|
)
|
|
3399
|
+
if cache_key is not None:
|
|
3400
|
+
_TOOL_CONVERT_CACHE[cache_key] = converted
|
|
3401
|
+
if len(_TOOL_CONVERT_CACHE) > _TOOL_CONVERT_CACHE_MAX:
|
|
3402
|
+
_TOOL_CONVERT_CACHE.popitem(last=False)
|
|
3378
3403
|
return converted
|
|
3379
3404
|
|
|
3380
3405
|
|
|
@@ -103,3 +103,32 @@ class LocalAdvisoryTest(unittest.TestCase):
|
|
|
103
103
|
def test_cloud_session_still_blocks(self):
|
|
104
104
|
rc, out = self._run({"ANTHROPIC_BASE_URL":"https://api.anthropic.com"})
|
|
105
105
|
self.assertEqual(rc, 2)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class LocalModeTest(unittest.TestCase):
|
|
109
|
+
def _run(self, env):
|
|
110
|
+
with _tf.TemporaryDirectory() as td:
|
|
111
|
+
root = _Path(td); (root/".git").mkdir()
|
|
112
|
+
f = root/"src"/"a.ts"; f.parent.mkdir(parents=True); f.write_text("x")
|
|
113
|
+
e = dict(_os.environ); e["UAP_REPO_ROOT"]=str(root)
|
|
114
|
+
for k in ("ANTHROPIC_BASE_URL","UAP_DELIVER_LOCAL_ADVISORY","UAP_DELIVER_LOCAL_MODE","UAP_DELIVER_ACTIVE"): e.pop(k, None)
|
|
115
|
+
e.update(env)
|
|
116
|
+
p = _sp.run([_sys.executable, str(_ENF), "--operation","Write","--args",_json.dumps({"file_path":str(f)})], capture_output=True, text=True, env=e)
|
|
117
|
+
return p.returncode, _json.loads(p.stdout) if p.stdout.strip() else {}
|
|
118
|
+
|
|
119
|
+
def test_local_mode_deliver_routes_through_deliver(self):
|
|
120
|
+
rc, out = self._run({"ANTHROPIC_BASE_URL":"http://127.0.0.1:4000","UAP_DELIVER_LOCAL_MODE":"deliver"})
|
|
121
|
+
self.assertEqual(rc, 2)
|
|
122
|
+
self.assertEqual(out.get("route"), "deliver")
|
|
123
|
+
|
|
124
|
+
def test_local_mode_advisory_allows(self):
|
|
125
|
+
rc, out = self._run({"ANTHROPIC_BASE_URL":"http://127.0.0.1:4000","UAP_DELIVER_LOCAL_MODE":"advisory"})
|
|
126
|
+
self.assertEqual(rc, 0); self.assertTrue(out["allowed"])
|
|
127
|
+
|
|
128
|
+
def test_local_mode_block_strict(self):
|
|
129
|
+
rc, out = self._run({"ANTHROPIC_BASE_URL":"http://127.0.0.1:4000","UAP_DELIVER_LOCAL_MODE":"block"})
|
|
130
|
+
self.assertEqual(rc, 2)
|
|
131
|
+
|
|
132
|
+
def test_default_is_advisory(self):
|
|
133
|
+
rc, out = self._run({"ANTHROPIC_BASE_URL":"http://127.0.0.1:4000"})
|
|
134
|
+
self.assertEqual(rc, 0)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Tests for A3: per-session tool-conversion cache."""
|
|
2
|
+
import importlib.util
|
|
3
|
+
import unittest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
proxy_path = Path(__file__).resolve().parents[3] / "tools" / "agents" / "scripts" / "anthropic_proxy.py"
|
|
7
|
+
spec = importlib.util.spec_from_file_location("anthropic_proxy", proxy_path)
|
|
8
|
+
ap = importlib.util.module_from_spec(spec)
|
|
9
|
+
spec.loader.exec_module(ap)
|
|
10
|
+
|
|
11
|
+
TOOLS = [
|
|
12
|
+
{"name": "Read", "description": "read", "input_schema": {"type": "object",
|
|
13
|
+
"properties": {"p": {"type": "string", "pattern": "^/.*"}}, "required": ["p"]}},
|
|
14
|
+
{"name": "Bash", "description": "run", "input_schema": {"type": "object",
|
|
15
|
+
"properties": {"cmd": {"type": "string"}}}},
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ToolConvertCacheTest(unittest.TestCase):
|
|
20
|
+
def setUp(self):
|
|
21
|
+
ap._TOOL_CONVERT_CACHE.clear()
|
|
22
|
+
|
|
23
|
+
def test_correct_conversion_and_sanitize(self):
|
|
24
|
+
out = ap._convert_anthropic_tools_to_openai(TOOLS)
|
|
25
|
+
self.assertEqual(out[0]["function"]["name"], "Read")
|
|
26
|
+
# regex pattern field stripped by sanitize
|
|
27
|
+
self.assertNotIn("pattern", out[0]["function"]["parameters"]["properties"]["p"])
|
|
28
|
+
|
|
29
|
+
def test_second_call_is_cache_hit_same_object(self):
|
|
30
|
+
a = ap._convert_anthropic_tools_to_openai(TOOLS)
|
|
31
|
+
self.assertEqual(len(ap._TOOL_CONVERT_CACHE), 1)
|
|
32
|
+
b = ap._convert_anthropic_tools_to_openai([dict(t) for t in TOOLS]) # equal-by-value
|
|
33
|
+
self.assertIs(a, b, "identical tool set must return the cached object")
|
|
34
|
+
|
|
35
|
+
def test_different_tools_miss(self):
|
|
36
|
+
ap._convert_anthropic_tools_to_openai(TOOLS)
|
|
37
|
+
ap._convert_anthropic_tools_to_openai(TOOLS[:1])
|
|
38
|
+
self.assertEqual(len(ap._TOOL_CONVERT_CACHE), 2)
|
|
39
|
+
|
|
40
|
+
def test_cache_bounded(self):
|
|
41
|
+
for i in range(ap._TOOL_CONVERT_CACHE_MAX + 5):
|
|
42
|
+
ap._convert_anthropic_tools_to_openai(
|
|
43
|
+
[{"name": f"T{i}", "description": "", "input_schema": {"type": "object"}}]
|
|
44
|
+
)
|
|
45
|
+
self.assertLessEqual(len(ap._TOOL_CONVERT_CACHE), ap._TOOL_CONVERT_CACHE_MAX)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
unittest.main()
|