@miller-tech/uap 1.218.0 → 1.219.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/cli/deliver.js +16 -0
- package/dist/cli/deliver.js.map +1 -1
- package/dist/cli/guided-setup.d.ts.map +1 -1
- package/dist/cli/guided-setup.js +8 -6
- package/dist/cli/guided-setup.js.map +1 -1
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +32 -0
- package/dist/cli/hooks.js.map +1 -1
- package/dist/cli/wizard-config.d.ts +2 -2
- package/dist/cli/wizard-config.d.ts.map +1 -1
- package/dist/config/settings-registry.d.ts.map +1 -1
- package/dist/config/settings-registry.js +22 -7
- package/dist/config/settings-registry.js.map +1 -1
- package/dist/coordination/reactor.d.ts +8 -0
- package/dist/coordination/reactor.d.ts.map +1 -1
- package/dist/coordination/reactor.js +43 -12
- package/dist/coordination/reactor.js.map +1 -1
- package/dist/mcp-router/tools/deliver.js +1 -1
- package/dist/mcp-router/tools/deliver.js.map +1 -1
- package/dist/types/config.d.ts +40 -16
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +10 -2
- package/dist/types/config.js.map +1 -1
- package/docs/reference/CONFIGURATION_REFERENCE.md +44 -8
- package/package.json +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/delivery_enforcement.py +226 -8
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/templates/hooks/deliver_autoroute.py +2 -2
- package/templates/hooks/escalation_tracker.py +252 -0
- package/templates/hooks/stop.sh +10 -0
- package/templates/hooks/uap-policy-gate.sh +5 -5
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +112 -4
|
@@ -71,7 +71,7 @@ export UAP_WORKTREE_ROOT="$CHECKOUT_ROOT"
|
|
|
71
71
|
# in .bashrc) can NOT silently downgrade a project configured to `block`
|
|
72
72
|
# and let the agent bypass deliver. `delivery.localMode` is read the same
|
|
73
73
|
# authoritative way and selects how a local-model session resolves block
|
|
74
|
-
# (advisory|deliver|block — see delivery_enforcement.py).
|
|
74
|
+
# (advisory|deliver|block|escalate — see delivery_enforcement.py).
|
|
75
75
|
# 2. ambient UAP_ENFORCE_DELIVERY (operator/CI per-run override) when the
|
|
76
76
|
# project declares nothing.
|
|
77
77
|
# 3. default: block.
|
|
@@ -83,7 +83,7 @@ import json, sys
|
|
|
83
83
|
try:
|
|
84
84
|
d = (json.load(open(sys.argv[1])).get("delivery") or {})
|
|
85
85
|
e = str(d.get("enforcement", "")).lower()
|
|
86
|
-
print("%s|%s" % (e if e in ("block", "advisory", "off") else "", str(d.get("localMode", "")).lower()))
|
|
86
|
+
print("%s|%s" % (e if e in ("block", "advisory", "off", "escalate") else "", str(d.get("localMode", "")).lower()))
|
|
87
87
|
except Exception:
|
|
88
88
|
print("|")
|
|
89
89
|
' "$MAIN_ROOT/.uap.json" 2>/dev/null || echo "|")"
|
|
@@ -168,7 +168,7 @@ if not hit and cmd:
|
|
|
168
168
|
if hit:
|
|
169
169
|
break
|
|
170
170
|
bypass = re.search(
|
|
171
|
-
r"UAP_DELIVER_BYPASS\s*=\s*[\x27\"]?1|UAP_ENFORCE_DELIVERY\s*=\s*[\x27\"]?(advisory|off|0|false|no)"
|
|
171
|
+
r"UAP_DELIVER_BYPASS\s*=\s*[\x27\"]?1|UAP_ENFORCE_DELIVERY\s*=\s*[\x27\"]?(advisory|off|escalate|0|false|no)"
|
|
172
172
|
r"|UAP_SELF_PROTECT_OFF\s*=\s*[\x27\"]?1|UAP_NO_WORKTREE\s*=\s*[\x27\"]?1"
|
|
173
173
|
r"|UAP_WORKDIR_SCOPE_OFF\s*=\s*[\x27\"]?1|UAP_USER_VALIDATION\s*=\s*[\x27\"]?0"
|
|
174
174
|
r"|UAP_DELIVER_NO_LOCK\s*=\s*[\x27\"]?1|UAP_NO_REVIEW\s*=\s*[\x27\"]?1"
|
|
@@ -293,7 +293,7 @@ record_execution() {
|
|
|
293
293
|
local esc_args esc_reason
|
|
294
294
|
esc_args="$(printf '%s' "$ARGS" | sed "s/'/''/g")"
|
|
295
295
|
esc_reason="$(printf '%s' "$reason" | sed "s/'/''/g" | cut -c1-500)"
|
|
296
|
-
sqlite3 "$DB" "INSERT INTO policy_executions
|
|
296
|
+
sqlite3 -cmd ".timeout 5000" "$DB" "INSERT INTO policy_executions
|
|
297
297
|
(policyId, toolName, operation, args, result, allowed, reason, executedAt)
|
|
298
298
|
VALUES ('$pid', '${TOOL//\'/\'\'}', '${TOOL//\'/\'\'}', '$esc_args',
|
|
299
299
|
'{}', $allowed, '$esc_reason', strftime('%Y-%m-%dT%H:%M:%fZ','now'));
|
|
@@ -463,7 +463,7 @@ except: print("")' 2>/dev/null || echo "")"
|
|
|
463
463
|
# first, and the agent saw the wrong routing message. Honor priority now so
|
|
464
464
|
# the intended policy fires first. (Blocking short-circuits at exit 2, so the
|
|
465
465
|
# highest-priority applicable block wins.) Name is the stable tiebreak.
|
|
466
|
-
done < <(sqlite3 "$DB" "SELECT p.id, p.name, t.toolName FROM policies p JOIN executable_tools t ON t.policyId=p.id WHERE p.isActive=1 ORDER BY p.priority DESC, p.name ASC;")
|
|
466
|
+
done < <(sqlite3 -cmd ".timeout 5000" "$DB" "SELECT p.id, p.name, t.toolName FROM policies p JOIN executable_tools t ON t.policyId=p.id WHERE p.isActive=1 ORDER BY p.priority DESC, p.name ASC;")
|
|
467
467
|
|
|
468
468
|
# A sensitive op that no self-protect enforcer ever evaluated = the control
|
|
469
469
|
# surface is unguarded (self-protect not registered/active). Fail closed.
|
|
Binary file
|
|
@@ -1945,6 +1945,96 @@ def _apply_profile_overrides(
|
|
|
1945
1945
|
return updated, prompt_suffix, grammar_text
|
|
1946
1946
|
|
|
1947
1947
|
|
|
1948
|
+
# ── Upstream-safe tool names ────────────────────────────────────────────────
|
|
1949
|
+
# llama.cpp's grammar builder cannot parse a tool NAME containing '-' or '.'
|
|
1950
|
+
# under tool_choice=required: the request fails with HTTP 400 "Failed to
|
|
1951
|
+
# initialize samplers: failed to parse grammar" (measured 2026-08-21 against
|
|
1952
|
+
# the live server: `uap-router_deliver` -> 400, the same schema as
|
|
1953
|
+
# `uap_router_deliver` -> 200; `auto` passes either way because the lazy
|
|
1954
|
+
# grammar path never compiles the name). OpenCode prefixes every MCP tool
|
|
1955
|
+
# with "<server>_" and servers are named with hyphens, so the moment a rail
|
|
1956
|
+
# forces `required` with such a tool present, the turn dies and the client
|
|
1957
|
+
# session wedges (observed: RECON hard tier restored `uap-router_deliver`,
|
|
1958
|
+
# forced required, 400, stream error, 10h idle).
|
|
1959
|
+
#
|
|
1960
|
+
# Names are rewritten to [A-Za-z0-9_] on the way UP (tool definitions, the
|
|
1961
|
+
# assistant history's tool_calls, a function-pinned tool_choice) and restored
|
|
1962
|
+
# on the way DOWN, so the client never sees the sanitized form. The map is
|
|
1963
|
+
# module-level: sanitization is a pure function of the name, so one map serves
|
|
1964
|
+
# every session and every response path (stream, strict, non-stream).
|
|
1965
|
+
_UPSTREAM_TOOL_NAME_BAD_RE = re.compile(r"[^A-Za-z0-9_]")
|
|
1966
|
+
_UPSTREAM_TOOL_NAME_MAP: dict[str, str] = {} # sanitized -> original
|
|
1967
|
+
|
|
1968
|
+
|
|
1969
|
+
def _sanitize_tool_name(name: str) -> str:
|
|
1970
|
+
return _UPSTREAM_TOOL_NAME_BAD_RE.sub("_", name or "")
|
|
1971
|
+
|
|
1972
|
+
|
|
1973
|
+
def _sanitize_tool_names_for_upstream(openai_body: dict) -> int:
|
|
1974
|
+
"""Rewrite unsafe tool names in-place; returns how many were renamed.
|
|
1975
|
+
|
|
1976
|
+
Idempotent: already-safe names are untouched, so calling it on a retry is
|
|
1977
|
+
harmless. A rename that would collide with another tool's (native or
|
|
1978
|
+
sanitized) name gets a numeric suffix.
|
|
1979
|
+
"""
|
|
1980
|
+
tools = openai_body.get("tools") or []
|
|
1981
|
+
originals = {
|
|
1982
|
+
((t.get("function") or {}).get("name") or "")
|
|
1983
|
+
for t in tools
|
|
1984
|
+
if isinstance(t, dict) and isinstance(t.get("function"), dict)
|
|
1985
|
+
}
|
|
1986
|
+
renamed: dict[str, str] = {} # original -> safe
|
|
1987
|
+
taken: set[str] = set()
|
|
1988
|
+
for t in tools:
|
|
1989
|
+
fn = t.get("function") if isinstance(t, dict) else None
|
|
1990
|
+
if not isinstance(fn, dict):
|
|
1991
|
+
continue
|
|
1992
|
+
name = fn.get("name") or ""
|
|
1993
|
+
safe = _sanitize_tool_name(name)
|
|
1994
|
+
if safe == name:
|
|
1995
|
+
taken.add(name)
|
|
1996
|
+
# A tool that NATIVELY owns this spelling must never be restored
|
|
1997
|
+
# to some other client's hyphenated name left in the global map.
|
|
1998
|
+
_UPSTREAM_TOOL_NAME_MAP.pop(name, None)
|
|
1999
|
+
continue
|
|
2000
|
+
base, i = safe, 2
|
|
2001
|
+
while safe in taken or (safe in originals and safe != name):
|
|
2002
|
+
safe = f"{base}_{i}"
|
|
2003
|
+
i += 1
|
|
2004
|
+
fn["name"] = safe
|
|
2005
|
+
taken.add(safe)
|
|
2006
|
+
renamed[name] = safe
|
|
2007
|
+
_UPSTREAM_TOOL_NAME_MAP[safe] = name
|
|
2008
|
+
if not renamed:
|
|
2009
|
+
return 0
|
|
2010
|
+
# The model must see ONE name per tool: history tool_calls that carry the
|
|
2011
|
+
# original spelling are rewritten too, as is a function-pinned tool_choice.
|
|
2012
|
+
for m in openai_body.get("messages") or []:
|
|
2013
|
+
if not isinstance(m, dict):
|
|
2014
|
+
continue
|
|
2015
|
+
for tc in m.get("tool_calls") or []:
|
|
2016
|
+
fn = tc.get("function") if isinstance(tc, dict) else None
|
|
2017
|
+
if isinstance(fn, dict) and fn.get("name") in renamed:
|
|
2018
|
+
fn["name"] = renamed[fn["name"]]
|
|
2019
|
+
choice = openai_body.get("tool_choice")
|
|
2020
|
+
if isinstance(choice, dict):
|
|
2021
|
+
fn = choice.get("function")
|
|
2022
|
+
if isinstance(fn, dict) and fn.get("name") in renamed:
|
|
2023
|
+
fn["name"] = renamed[fn["name"]]
|
|
2024
|
+
logger.info(
|
|
2025
|
+
"TOOL NAMES: sanitized %d upstream-unsafe tool name(s) for llama.cpp grammar: %s",
|
|
2026
|
+
len(renamed), ", ".join(f"{k}->{v}" for k, v in list(renamed.items())[:6]),
|
|
2027
|
+
)
|
|
2028
|
+
return len(renamed)
|
|
2029
|
+
|
|
2030
|
+
|
|
2031
|
+
def _restore_tool_name(name: str) -> str:
|
|
2032
|
+
"""The client-facing name for a tool the upstream called by sanitized name."""
|
|
2033
|
+
if not name or not _UPSTREAM_TOOL_NAME_MAP:
|
|
2034
|
+
return name
|
|
2035
|
+
return _UPSTREAM_TOOL_NAME_MAP.get(name, name)
|
|
2036
|
+
|
|
2037
|
+
|
|
1948
2038
|
def _is_grammar_tools_incompatibility(status_code: int, error_text: str) -> bool:
|
|
1949
2039
|
if status_code != 400:
|
|
1950
2040
|
return False
|
|
@@ -1963,9 +2053,15 @@ def _is_gemma4_peg_parse_failure(status_code: int, error_text: str) -> bool:
|
|
|
1963
2053
|
tool_choice='auto' so the model can emit prose or a complete call
|
|
1964
2054
|
without grammar enforcement triggering this failure mode.
|
|
1965
2055
|
"""
|
|
2056
|
+
text = error_text or ""
|
|
2057
|
+
# Same remedy for the other grammar-compile failure: HTTP 400 "Failed to
|
|
2058
|
+
# initialize samplers: failed to parse grammar" (a tool schema/name the
|
|
2059
|
+
# builder cannot express under `required`). Relaxing to `auto` lets the
|
|
2060
|
+
# turn proceed on the lazy grammar path instead of killing the stream.
|
|
2061
|
+
if status_code == 400 and "failed to parse grammar" in text.lower():
|
|
2062
|
+
return True
|
|
1966
2063
|
if status_code != 500:
|
|
1967
2064
|
return False
|
|
1968
|
-
text = error_text or ""
|
|
1969
2065
|
return (
|
|
1970
2066
|
"Failed to parse input at pos" in text
|
|
1971
2067
|
or "<|tool_call>call:" in text
|
|
@@ -8844,6 +8940,12 @@ def _tool_schema_map_from_anthropic_body(anthropic_body: dict) -> dict[str, dict
|
|
|
8844
8940
|
if isinstance(name, str) and name:
|
|
8845
8941
|
schema = tool.get("input_schema")
|
|
8846
8942
|
schema_map[name] = schema if isinstance(schema, dict) else {}
|
|
8943
|
+
# The upstream calls tools by their SANITIZED name (see
|
|
8944
|
+
# _sanitize_tool_names_for_upstream); a guardrail that only knew the
|
|
8945
|
+
# client's spelling would reject every such call as "unknown tool".
|
|
8946
|
+
safe = _sanitize_tool_name(name)
|
|
8947
|
+
if safe != name and safe not in schema_map:
|
|
8948
|
+
schema_map[safe] = schema_map[name]
|
|
8847
8949
|
return schema_map
|
|
8848
8950
|
|
|
8849
8951
|
|
|
@@ -9317,6 +9419,9 @@ def _anthropic_tools_by_name(anthropic_body: dict) -> dict[str, dict]:
|
|
|
9317
9419
|
else {}
|
|
9318
9420
|
)
|
|
9319
9421
|
tool_map[name] = schema or {}
|
|
9422
|
+
safe = _sanitize_tool_name(name) # upstream spelling, see schema map above
|
|
9423
|
+
if safe != name and safe not in tool_map:
|
|
9424
|
+
tool_map[safe] = tool_map[name]
|
|
9320
9425
|
return tool_map
|
|
9321
9426
|
|
|
9322
9427
|
|
|
@@ -11202,7 +11307,7 @@ def openai_to_anthropic_response(
|
|
|
11202
11307
|
{
|
|
11203
11308
|
"type": "tool_use",
|
|
11204
11309
|
"id": tool_use_id,
|
|
11205
|
-
"name": fn.get("name", ""),
|
|
11310
|
+
"name": _restore_tool_name(fn.get("name", "")),
|
|
11206
11311
|
"input": args,
|
|
11207
11312
|
}
|
|
11208
11313
|
)
|
|
@@ -11561,7 +11666,7 @@ async def stream_anthropic_response(
|
|
|
11561
11666
|
initial_args = fn.get("arguments", "")
|
|
11562
11667
|
tool_calls_by_index[tc_idx] = {
|
|
11563
11668
|
"id": tc_id,
|
|
11564
|
-
"name": fn.get("name", ""),
|
|
11669
|
+
"name": _restore_tool_name(fn.get("name", "")),
|
|
11565
11670
|
"arguments": initial_args,
|
|
11566
11671
|
"block_index": tool_block_index,
|
|
11567
11672
|
}
|
|
@@ -11749,7 +11854,7 @@ async def stream_anthropic_response(
|
|
|
11749
11854
|
for idx, xtc in enumerate(xml_extracted, start=1):
|
|
11750
11855
|
fn = xtc.get("function", {})
|
|
11751
11856
|
tc_id = xtc.get("id", f"toolu_{uuid.uuid4().hex[:12]}")
|
|
11752
|
-
tc_name = fn.get("name", "")
|
|
11857
|
+
tc_name = _restore_tool_name(fn.get("name", ""))
|
|
11753
11858
|
tc_args = fn.get("arguments", "{}")
|
|
11754
11859
|
tool_calls_by_index[idx] = {
|
|
11755
11860
|
"id": tc_id,
|
|
@@ -12380,6 +12485,9 @@ async def messages(request: Request):
|
|
|
12380
12485
|
media_type="application/json",
|
|
12381
12486
|
)
|
|
12382
12487
|
|
|
12488
|
+
# Last step before the wire: names llama.cpp's grammar builder can parse.
|
|
12489
|
+
_sanitize_tool_names_for_upstream(openai_body)
|
|
12490
|
+
|
|
12383
12491
|
use_guarded_non_stream = _should_use_guarded_non_stream(
|
|
12384
12492
|
is_stream,
|
|
12385
12493
|
body,
|