@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.83.0",
3
+ "version": "1.84.1",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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 plain local-model session cannot effectively route through deliver and
69
- # deadlocks under block mode (it can read but never write -> recon loop, observed
70
- # live). Downgrade block->advisory for local sessions so the model can write
71
- # directly; the local proxy already guards it. Set UAP_DELIVER_LOCAL_ADVISORY=0
72
- # to keep strict block for local sessions too.
73
- _LOCAL_ADVISORY = os.environ.get("UAP_DELIVER_LOCAL_ADVISORY", "on").lower() not in {
74
- "0",
75
- "off",
76
- "false",
77
- "no",
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 _LOCAL_ADVISORY and _is_local_model_session():
128
- mode = "advisory" # #2: unblock plain local-model sessions
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
@@ -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()