@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 +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/templates/hooks/deliver_autoroute.py +124 -0
- package/templates/hooks/uap-policy-gate.sh +12 -2
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/tests/test_deliver_autoroute.py +73 -0
package/package.json
CHANGED
|
Binary file
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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;")
|
|
Binary file
|
|
@@ -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()
|