@miller-tech/uap 1.79.0 → 1.80.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.79.0",
3
+ "version": "1.80.0",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env python3
2
+ """deliver-autoroute: R1 follow-up — consume a delivery-enforcement block's
3
+ `route:deliver` signal at the harness boundary.
4
+
5
+ The policy-gate hook pipes a blocked enforcer's JSON output to this helper. When
6
+ the block carries route == "deliver", the helper:
7
+ 1. Logs the blocked intent to the project's .uap pending-deliver log (so the
8
+ intent is never lost).
9
+ 2. If UAP_DELIVER_AUTOROUTE is on, spawns `uap deliver "<hint>"` detached in
10
+ the background (deduped per file so a retrying model does not fan out
11
+ dozens of runs), and annotates the message.
12
+ 3. Prints the (possibly annotated) block message on stdout for the hook to
13
+ surface to the agent.
14
+
15
+ Default (autoroute off): just logs the intent + returns the message unchanged —
16
+ no behavior change. The hook still blocks (exit 2); this only enriches the block.
17
+ """
18
+ import argparse
19
+ import json
20
+ import os
21
+ import sys
22
+ import time
23
+ from pathlib import Path
24
+
25
+ PENDING_LOG = "pending-deliver.jsonl"
26
+ SEEN_FILE = "autoroute-seen"
27
+ UAP_DIR = ".uap"
28
+
29
+
30
+ def _autoroute_enabled() -> bool:
31
+ v = os.environ.get("UAP_DELIVER_AUTOROUTE", "").lower()
32
+ return v not in {"", "0", "off", "false", "no"}
33
+
34
+
35
+ def decide(out: dict, tool: str, args: dict, autoroute_on: bool, seen_files: set) -> dict:
36
+ """Pure decision: what message to show, whether to spawn, and the intent."""
37
+ reason = out.get("reason", "")
38
+ route = out.get("route")
39
+ hint = out.get("deliverHint") or ""
40
+ file_path = args.get("file_path") or args.get("path") or args.get("target") or ""
41
+
42
+ if route != "deliver":
43
+ return {"message": reason, "route": route, "spawn": False,
44
+ "file_path": file_path, "hint": hint, "intent": None}
45
+
46
+ intent = {"ts": int(time.time()), "tool": tool, "file_path": file_path, "hint": hint}
47
+ spawn = bool(autoroute_on and hint and file_path and file_path not in seen_files)
48
+ message = reason
49
+ if spawn:
50
+ message = reason + " [auto-routed to `uap deliver` — running in the background]"
51
+ elif autoroute_on and file_path in seen_files:
52
+ message = reason + " [already auto-routed to `uap deliver` this session — wait for it]"
53
+ return {"message": message, "route": route, "spawn": spawn,
54
+ "file_path": file_path, "hint": hint, "intent": intent}
55
+
56
+
57
+ def _seen_path(root: Path) -> Path:
58
+ return root / UAP_DIR / SEEN_FILE
59
+
60
+
61
+ def _load_seen(root: Path) -> set:
62
+ try:
63
+ return set(l.strip() for l in _seen_path(root).read_text().splitlines() if l.strip())
64
+ except Exception:
65
+ return set()
66
+
67
+
68
+ def _spawn_deliver(root: Path, hint: str) -> None:
69
+ """Spawn `uap deliver "<hint>"` fully detached. Best-effort; never raises."""
70
+ import subprocess
71
+ try:
72
+ subprocess.Popen(
73
+ ["uap", "deliver", hint],
74
+ cwd=str(root),
75
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL,
76
+ start_new_session=True,
77
+ )
78
+ except Exception:
79
+ pass
80
+
81
+
82
+ def main() -> None:
83
+ ap = argparse.ArgumentParser()
84
+ ap.add_argument("--tool", default="")
85
+ ap.add_argument("--args", default="{}")
86
+ ap.add_argument("--root", default=".")
87
+ ap.add_argument("--policy", default="")
88
+ ns = ap.parse_args()
89
+
90
+ try:
91
+ out = json.loads(sys.stdin.read() or "{}")
92
+ except Exception:
93
+ out = {}
94
+ try:
95
+ args = json.loads(ns.args or "{}")
96
+ except Exception:
97
+ args = {}
98
+
99
+ root = Path(ns.root)
100
+ d = decide(out, ns.tool, args, _autoroute_enabled(), _load_seen(root))
101
+
102
+ if d["intent"] is not None:
103
+ try:
104
+ uap_dir = root / UAP_DIR
105
+ uap_dir.mkdir(parents=True, exist_ok=True)
106
+ with (uap_dir / PENDING_LOG).open("a") as f:
107
+ f.write(json.dumps(d["intent"]) + "\n")
108
+ except Exception:
109
+ pass
110
+
111
+ if d["spawn"]:
112
+ try:
113
+ with _seen_path(root).open("a") as f:
114
+ f.write(d["file_path"] + "\n")
115
+ except Exception:
116
+ pass
117
+ _spawn_deliver(root, d["hint"])
118
+
119
+ prefix = ("[UAP policy blocked: " + ns.policy + "] ") if ns.policy else ""
120
+ sys.stdout.write(prefix + d["message"])
121
+
122
+
123
+ if __name__ == "__main__":
124
+ main()
@@ -55,10 +55,20 @@ while IFS='|' read -r pid pname tool; do
55
55
  try: d=json.loads(sys.stdin.read()); print("1" if d.get("allowed",True) else "0")
56
56
  except: print("1")' 2>/dev/null || echo 1)"
57
57
  if [[ "$allowed" == "0" ]]; then
58
- reason="$(printf '%s' "$out" | python3 -c 'import json,sys;
58
+ # R1: consume the enforcer's route:deliver signal (log intent, opt-in
59
+ # background auto-route to `uap deliver`). Falls back to the plain reason if
60
+ # the helper is missing/fails.
61
+ msg=""
62
+ if [[ -f "$HOOK_DIR/deliver_autoroute.py" ]]; then
63
+ msg="$(printf '%s' "$out" | python3 "$HOOK_DIR/deliver_autoroute.py" --tool "$TOOL" --args "$ARGS" --root "$MAIN_ROOT" --policy "$pname" 2>/dev/null || true)"
64
+ fi
65
+ if [[ -z "$msg" ]]; then
66
+ reason="$(printf '%s' "$out" | python3 -c 'import json,sys;
59
67
  try: print(json.loads(sys.stdin.read()).get("reason",""))
60
68
  except: print("")' 2>/dev/null || echo "")"
61
- echo "[UAP policy blocked: $pname] $reason" >&2
69
+ msg="[UAP policy blocked: $pname] $reason"
70
+ fi
71
+ echo "$msg" >&2
62
72
  exit 2
63
73
  fi
64
74
  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;")
@@ -0,0 +1,73 @@
1
+ """Tests for deliver-autoroute (R1 follow-up): consume route:deliver."""
2
+ import importlib.util
3
+ import json
4
+ import os
5
+ import tempfile
6
+ import unittest
7
+ from pathlib import Path
8
+
9
+ HELPER = Path(__file__).resolve().parents[3] / "templates" / "hooks" / "deliver_autoroute.py"
10
+ spec = importlib.util.spec_from_file_location("deliver_autoroute", HELPER)
11
+ mod = importlib.util.module_from_spec(spec)
12
+ spec.loader.exec_module(mod)
13
+
14
+ BLOCK_OUT = {
15
+ "allowed": False,
16
+ "reason": "BLOCKED: do not edit src/foo.ts directly.",
17
+ "route": "deliver",
18
+ "deliverHint": 'uap deliver "implement the intended change to src/foo.ts"',
19
+ }
20
+ ARGS = {"file_path": "/repo/src/foo.ts"}
21
+
22
+
23
+ class DecideTest(unittest.TestCase):
24
+ def test_non_deliver_route_is_passthrough(self):
25
+ d = mod.decide({"reason": "blocked", "route": "worktree"}, "Write", ARGS, True, set())
26
+ self.assertFalse(d["spawn"])
27
+ self.assertIsNone(d["intent"])
28
+ self.assertEqual(d["message"], "blocked")
29
+
30
+ def test_deliver_route_logs_intent_no_spawn_when_off(self):
31
+ d = mod.decide(BLOCK_OUT, "Write", ARGS, False, set())
32
+ self.assertFalse(d["spawn"])
33
+ self.assertIsNotNone(d["intent"])
34
+ self.assertEqual(d["intent"]["file_path"], "/repo/src/foo.ts")
35
+ self.assertEqual(d["message"], BLOCK_OUT["reason"]) # unchanged
36
+
37
+ def test_deliver_route_spawns_when_on_and_unseen(self):
38
+ d = mod.decide(BLOCK_OUT, "Write", ARGS, True, set())
39
+ self.assertTrue(d["spawn"])
40
+ self.assertIn("auto-routed", d["message"])
41
+
42
+ def test_deliver_route_dedupes_seen_file(self):
43
+ d = mod.decide(BLOCK_OUT, "Write", ARGS, True, {"/repo/src/foo.ts"})
44
+ self.assertFalse(d["spawn"])
45
+ self.assertIn("already auto-routed", d["message"])
46
+
47
+ def test_no_spawn_without_hint(self):
48
+ out = dict(BLOCK_OUT); out["deliverHint"] = ""
49
+ d = mod.decide(out, "Write", ARGS, True, set())
50
+ self.assertFalse(d["spawn"])
51
+
52
+
53
+ class LoggingTest(unittest.TestCase):
54
+ def test_main_logs_pending_intent(self):
55
+ with tempfile.TemporaryDirectory() as td:
56
+ root = Path(td)
57
+ # invoke main() via subprocess to exercise the full path (autoroute off)
58
+ import subprocess, sys
59
+ env = dict(os.environ); env.pop("UAP_DELIVER_AUTOROUTE", None)
60
+ p = subprocess.run(
61
+ [sys.executable, str(HELPER), "--tool", "Write",
62
+ "--args", json.dumps(ARGS), "--root", str(root), "--policy", "delivery-enforcement"],
63
+ input=json.dumps(BLOCK_OUT), capture_output=True, text=True, env=env,
64
+ )
65
+ self.assertIn("BLOCKED", p.stdout)
66
+ log = root / ".uap" / "pending-deliver.jsonl"
67
+ self.assertTrue(log.exists(), "intent must be logged")
68
+ rec = json.loads(log.read_text().strip())
69
+ self.assertEqual(rec["file_path"], "/repo/src/foo.ts")
70
+
71
+
72
+ if __name__ == "__main__":
73
+ unittest.main()