@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.
Files changed (35) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/cli/deliver.js +16 -0
  3. package/dist/cli/deliver.js.map +1 -1
  4. package/dist/cli/guided-setup.d.ts.map +1 -1
  5. package/dist/cli/guided-setup.js +8 -6
  6. package/dist/cli/guided-setup.js.map +1 -1
  7. package/dist/cli/hooks.d.ts.map +1 -1
  8. package/dist/cli/hooks.js +32 -0
  9. package/dist/cli/hooks.js.map +1 -1
  10. package/dist/cli/wizard-config.d.ts +2 -2
  11. package/dist/cli/wizard-config.d.ts.map +1 -1
  12. package/dist/config/settings-registry.d.ts.map +1 -1
  13. package/dist/config/settings-registry.js +22 -7
  14. package/dist/config/settings-registry.js.map +1 -1
  15. package/dist/coordination/reactor.d.ts +8 -0
  16. package/dist/coordination/reactor.d.ts.map +1 -1
  17. package/dist/coordination/reactor.js +43 -12
  18. package/dist/coordination/reactor.js.map +1 -1
  19. package/dist/mcp-router/tools/deliver.js +1 -1
  20. package/dist/mcp-router/tools/deliver.js.map +1 -1
  21. package/dist/types/config.d.ts +40 -16
  22. package/dist/types/config.d.ts.map +1 -1
  23. package/dist/types/config.js +10 -2
  24. package/dist/types/config.js.map +1 -1
  25. package/docs/reference/CONFIGURATION_REFERENCE.md +44 -8
  26. package/package.json +1 -1
  27. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  28. package/src/policies/enforcers/delivery_enforcement.py +226 -8
  29. package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
  30. package/templates/hooks/deliver_autoroute.py +2 -2
  31. package/templates/hooks/escalation_tracker.py +252 -0
  32. package/templates/hooks/stop.sh +10 -0
  33. package/templates/hooks/uap-policy-gate.sh +5 -5
  34. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  35. 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.
@@ -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,