@miller-tech/uap 1.15.7 → 1.15.8

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.15.7",
3
+ "version": "1.15.8",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -76,6 +76,7 @@ Dependencies
76
76
  """
77
77
 
78
78
  import asyncio
79
+ import copy
79
80
  import hashlib
80
81
  import json
81
82
  import logging
@@ -1915,6 +1916,20 @@ def _sanitize_markup_value(value):
1915
1916
  return value, False
1916
1917
 
1917
1918
 
1919
+ _REQUIRED_PLACEHOLDER = "__uap_required__"
1920
+ _MISSING_REQUIRED_VALUE = object()
1921
+
1922
+
1923
+ def _contains_required_placeholder(value) -> bool:
1924
+ if isinstance(value, str):
1925
+ return value.strip() == _REQUIRED_PLACEHOLDER
1926
+ if isinstance(value, list):
1927
+ return any(_contains_required_placeholder(item) for item in value)
1928
+ if isinstance(value, dict):
1929
+ return any(_contains_required_placeholder(item) for item in value.values())
1930
+ return False
1931
+
1932
+
1918
1933
  def _repair_tool_call_markup(openai_resp: dict) -> tuple[dict, int]:
1919
1934
  if not _openai_has_tool_calls(openai_resp):
1920
1935
  return openai_resp, 0
@@ -1986,33 +2001,30 @@ def _repair_tool_call_markup(openai_resp: dict) -> tuple[dict, int]:
1986
2001
 
1987
2002
 
1988
2003
  def _default_required_value(field_name: str, field_schema: dict):
1989
- expected_type = field_schema.get("type") if isinstance(field_schema, dict) else None
1990
- if isinstance(expected_type, list):
1991
- expected_type = expected_type[0] if expected_type else "string"
2004
+ _ = field_name
2005
+ if not isinstance(field_schema, dict):
2006
+ return _MISSING_REQUIRED_VALUE
2007
+
2008
+ if "default" in field_schema:
2009
+ default_value = copy.deepcopy(field_schema.get("default"))
2010
+ if not _contains_required_placeholder(default_value):
2011
+ return default_value
2012
+
2013
+ enum_values = field_schema.get("enum")
2014
+ if isinstance(enum_values, list):
2015
+ for candidate in enum_values:
2016
+ if _required_value_is_empty(candidate):
2017
+ continue
2018
+ if _contains_required_placeholder(candidate):
2019
+ continue
2020
+ return copy.deepcopy(candidate)
1992
2021
 
1993
- if expected_type == "integer":
1994
- return 0
1995
- if expected_type == "number":
1996
- return 0
1997
- if expected_type == "boolean":
1998
- return False
1999
- if expected_type == "object":
2000
- return {"value": "__uap_required__"}
2001
- if expected_type == "array":
2002
- return ["__uap_required__"]
2003
-
2004
- key = (field_name or "").lower()
2005
- if key in {"command", "cmd"}:
2006
- return "pwd"
2007
- if key == "cron":
2008
- return "* * * * *"
2009
- if key in {"pattern", "glob"}:
2010
- return "*"
2011
- if key == "subject":
2012
- return "task"
2013
- if key in {"path", "file", "filepath", "file_path"} or key.endswith("_path"):
2014
- return "."
2015
- return "__uap_required__"
2022
+ if "const" in field_schema:
2023
+ const_value = copy.deepcopy(field_schema.get("const"))
2024
+ if not _contains_required_placeholder(const_value):
2025
+ return const_value
2026
+
2027
+ return _MISSING_REQUIRED_VALUE
2016
2028
 
2017
2029
 
2018
2030
  def _repair_required_tool_args(
@@ -2075,7 +2087,10 @@ def _repair_required_tool_args(
2075
2087
  if isinstance(properties.get(field), dict)
2076
2088
  else {}
2077
2089
  )
2078
- parsed_args[field] = _default_required_value(field, field_schema)
2090
+ fallback_value = _default_required_value(field, field_schema)
2091
+ if fallback_value is _MISSING_REQUIRED_VALUE:
2092
+ continue
2093
+ parsed_args[field] = fallback_value
2079
2094
  changed = True
2080
2095
 
2081
2096
  if not changed:
@@ -2298,6 +2313,18 @@ def _validate_tool_call_arguments(
2298
2313
  ),
2299
2314
  )
2300
2315
 
2316
+ if _contains_required_placeholder(parsed):
2317
+ return ToolResponseIssue(
2318
+ kind="invalid_tool_args",
2319
+ reason=(
2320
+ f"arguments for '{tool_name}' contain unresolved placeholder values"
2321
+ ),
2322
+ retry_hint=(
2323
+ f"Emit exactly one `{tool_name}` tool call with real schema-valid arguments. "
2324
+ f"Never emit `{_REQUIRED_PLACEHOLDER}` placeholders."
2325
+ ),
2326
+ )
2327
+
2301
2328
  if not isinstance(tool_schema, dict):
2302
2329
  tool_schema = {}
2303
2330
 
@@ -2312,6 +2339,7 @@ def _validate_tool_call_arguments(
2312
2339
  missing: list[str] = []
2313
2340
  empty: list[str] = []
2314
2341
  wrong_type: list[str] = []
2342
+ enum_mismatch: list[str] = []
2315
2343
 
2316
2344
  for field in required:
2317
2345
  if not isinstance(field, str):
@@ -2334,6 +2362,15 @@ def _validate_tool_call_arguments(
2334
2362
  wrong_type.append(field)
2335
2363
  continue
2336
2364
 
2365
+ enum_values = schema.get("enum")
2366
+ if isinstance(enum_values, list) and enum_values and value not in enum_values:
2367
+ enum_mismatch.append(field)
2368
+ continue
2369
+
2370
+ if "const" in schema and value != schema.get("const"):
2371
+ enum_mismatch.append(field)
2372
+ continue
2373
+
2337
2374
  min_length = schema.get("minLength")
2338
2375
  if (
2339
2376
  isinstance(min_length, int)
@@ -2351,7 +2388,7 @@ def _validate_tool_call_arguments(
2351
2388
  ):
2352
2389
  empty.append(field)
2353
2390
 
2354
- if missing or empty or wrong_type:
2391
+ if missing or empty or wrong_type or enum_mismatch:
2355
2392
  details = []
2356
2393
  if missing:
2357
2394
  details.append(f"missing: {', '.join(missing)}")
@@ -2359,6 +2396,8 @@ def _validate_tool_call_arguments(
2359
2396
  details.append(f"empty: {', '.join(empty)}")
2360
2397
  if wrong_type:
2361
2398
  details.append(f"type mismatch: {', '.join(wrong_type)}")
2399
+ if enum_mismatch:
2400
+ details.append(f"enum mismatch: {', '.join(enum_mismatch)}")
2362
2401
  required_fields = ", ".join(str(f) for f in required if isinstance(f, str))
2363
2402
  required_hint = (
2364
2403
  f"Required fields must be non-empty: {required_fields}. "
@@ -643,7 +643,11 @@ class TestMalformedToolGuardrail(unittest.TestCase):
643
643
  "type": "object",
644
644
  "required": ["cron", "command"],
645
645
  "properties": {
646
- "cron": {"type": "string", "minLength": 1},
646
+ "cron": {
647
+ "type": "string",
648
+ "minLength": 1,
649
+ "default": "* * * * *",
650
+ },
647
651
  "command": {"type": "string", "minLength": 1},
648
652
  },
649
653
  },
@@ -956,9 +960,21 @@ class TestMalformedToolGuardrail(unittest.TestCase):
956
960
  "type": "object",
957
961
  "required": ["cron", "pattern", "subject"],
958
962
  "properties": {
959
- "cron": {"type": "string", "minLength": 1},
960
- "pattern": {"type": "string", "minLength": 1},
961
- "subject": {"type": "string", "minLength": 1},
963
+ "cron": {
964
+ "type": "string",
965
+ "minLength": 1,
966
+ "default": "* * * * *",
967
+ },
968
+ "pattern": {
969
+ "type": "string",
970
+ "minLength": 1,
971
+ "default": "*",
972
+ },
973
+ "subject": {
974
+ "type": "string",
975
+ "minLength": 1,
976
+ "default": "task",
977
+ },
962
978
  },
963
979
  },
964
980
  }
@@ -1008,9 +1024,21 @@ class TestMalformedToolGuardrail(unittest.TestCase):
1008
1024
  "type": "object",
1009
1025
  "required": ["cron", "pattern", "subject"],
1010
1026
  "properties": {
1011
- "cron": {"type": "string", "minLength": 1},
1012
- "pattern": {"type": "string", "minLength": 1},
1013
- "subject": {"type": "string", "minLength": 1},
1027
+ "cron": {
1028
+ "type": "string",
1029
+ "minLength": 1,
1030
+ "default": "* * * * *",
1031
+ },
1032
+ "pattern": {
1033
+ "type": "string",
1034
+ "minLength": 1,
1035
+ "default": "*",
1036
+ },
1037
+ "subject": {
1038
+ "type": "string",
1039
+ "minLength": 1,
1040
+ "default": "task",
1041
+ },
1014
1042
  },
1015
1043
  },
1016
1044
  }
@@ -1134,10 +1162,7 @@ class TestMalformedToolGuardrail(unittest.TestCase):
1134
1162
  )
1135
1163
  self.assertTrue(args["cron"].strip())
1136
1164
  self.assertTrue(args["command"].strip())
1137
- self.assertTrue(
1138
- monitor.arg_preflight_repairs >= 1
1139
- or monitor.arg_preflight_rejections >= 1
1140
- )
1165
+ self.assertGreaterEqual(len(fake_client.requests), 1)
1141
1166
  if fake_client.requests:
1142
1167
  retry_payload = fake_client.requests[0]["kwargs"]["json"]
1143
1168
  repair_message = retry_payload["messages"][-1]["content"]
@@ -1488,6 +1513,139 @@ class TestToolTurnControls(unittest.TestCase):
1488
1513
  setattr(proxy, "PROXY_ANALYSIS_ONLY_MAX_MESSAGES", old_max_messages)
1489
1514
 
1490
1515
 
1516
+ class TestRequiredArgRepair(unittest.TestCase):
1517
+ def test_repair_required_args_uses_schema_enum_value(self):
1518
+ openai_resp = {
1519
+ "choices": [
1520
+ {
1521
+ "message": {
1522
+ "tool_calls": [
1523
+ {
1524
+ "id": "call_1",
1525
+ "function": {
1526
+ "name": "omp_task",
1527
+ "arguments": '{"prompt":"analyze"}',
1528
+ },
1529
+ }
1530
+ ]
1531
+ }
1532
+ }
1533
+ ]
1534
+ }
1535
+ anthropic_body = {
1536
+ "tools": [
1537
+ {
1538
+ "name": "omp_task",
1539
+ "input_schema": {
1540
+ "type": "object",
1541
+ "required": ["agent", "prompt"],
1542
+ "properties": {
1543
+ "agent": {
1544
+ "type": "string",
1545
+ "enum": ["task", "explore", "plan"],
1546
+ },
1547
+ "prompt": {"type": "string"},
1548
+ },
1549
+ },
1550
+ }
1551
+ ]
1552
+ }
1553
+
1554
+ repaired, repaired_count = proxy._repair_required_tool_args(
1555
+ openai_resp, anthropic_body
1556
+ )
1557
+
1558
+ self.assertEqual(repaired_count, 1)
1559
+ args = json.loads(
1560
+ repaired["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"]
1561
+ )
1562
+ self.assertEqual(args["agent"], "task")
1563
+
1564
+ def test_repair_required_args_does_not_inject_placeholder_without_schema_defaults(
1565
+ self,
1566
+ ):
1567
+ openai_resp = {
1568
+ "choices": [
1569
+ {
1570
+ "message": {
1571
+ "tool_calls": [
1572
+ {
1573
+ "id": "call_1",
1574
+ "function": {
1575
+ "name": "omp_task",
1576
+ "arguments": '{"prompt":"analyze"}',
1577
+ },
1578
+ }
1579
+ ]
1580
+ }
1581
+ }
1582
+ ]
1583
+ }
1584
+ anthropic_body = {
1585
+ "tools": [
1586
+ {
1587
+ "name": "omp_task",
1588
+ "input_schema": {
1589
+ "type": "object",
1590
+ "required": ["agent", "prompt"],
1591
+ "properties": {
1592
+ "agent": {"type": "string"},
1593
+ "prompt": {"type": "string"},
1594
+ },
1595
+ },
1596
+ }
1597
+ ]
1598
+ }
1599
+
1600
+ repaired, repaired_count = proxy._repair_required_tool_args(
1601
+ openai_resp, anthropic_body
1602
+ )
1603
+
1604
+ self.assertEqual(repaired_count, 0)
1605
+ args = json.loads(
1606
+ repaired["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"]
1607
+ )
1608
+ self.assertNotIn("agent", args)
1609
+
1610
+ def test_validate_tool_args_rejects_placeholder_values(self):
1611
+ issue = proxy._validate_tool_call_arguments(
1612
+ "omp_task",
1613
+ '{"agent":"__uap_required__","prompt":"analyze"}',
1614
+ {
1615
+ "type": "object",
1616
+ "required": ["agent", "prompt"],
1617
+ "properties": {
1618
+ "agent": {"type": "string", "enum": ["task", "explore"]},
1619
+ "prompt": {"type": "string"},
1620
+ },
1621
+ },
1622
+ {"omp_task"},
1623
+ )
1624
+
1625
+ self.assertTrue(issue.has_issue())
1626
+ self.assertEqual(issue.kind, "invalid_tool_args")
1627
+ self.assertIn("placeholder", issue.reason)
1628
+
1629
+ def test_validate_tool_args_rejects_enum_mismatch(self):
1630
+ issue = proxy._validate_tool_call_arguments(
1631
+ "omp_task",
1632
+ '{"agent":"planner","prompt":"analyze"}',
1633
+ {
1634
+ "type": "object",
1635
+ "required": ["agent", "prompt"],
1636
+ "properties": {
1637
+ "agent": {"type": "string", "enum": ["task", "explore"]},
1638
+ "prompt": {"type": "string"},
1639
+ },
1640
+ },
1641
+ {"omp_task"},
1642
+ )
1643
+
1644
+ self.assertTrue(issue.has_issue())
1645
+ self.assertEqual(issue.kind, "invalid_tool_args")
1646
+ self.assertIn("enum mismatch", issue.reason)
1647
+
1648
+
1491
1649
  class TestSessionContaminationBreaker(unittest.TestCase):
1492
1650
  def test_contamination_breaker_trims_and_resets_streak(self):
1493
1651
  old_enabled = getattr(proxy, "PROXY_SESSION_CONTAMINATION_BREAKER")