@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
|
@@ -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
|
-
|
|
1990
|
-
if isinstance(
|
|
1991
|
-
|
|
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
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
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
|
-
|
|
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": {
|
|
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": {
|
|
960
|
-
|
|
961
|
-
|
|
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": {
|
|
1012
|
-
|
|
1013
|
-
|
|
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.
|
|
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")
|