@miller-tech/uap 1.78.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.78.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",
@@ -100,7 +100,16 @@ def main() -> None:
100
100
 
101
101
  mode = os.environ.get("UAP_ENFORCE_DELIVERY", "block").lower()
102
102
  if mode == "block":
103
- emit(False, msg)
103
+ # R1: emit a machine-actionable routing signal so a capable harness can
104
+ # auto-route the blocked change INTO `uap deliver` instead of leaving the
105
+ # agent to flail. `route`/`deliverHint` are advisory extra fields; harness
106
+ # adapters that understand them route, others just show `reason`.
107
+ emit(
108
+ False,
109
+ msg,
110
+ route="deliver",
111
+ deliverHint=f'uap deliver "implement the intended change to {rel_posix}"',
112
+ )
104
113
 
105
114
  # Advisory (opt-out): never blocks. Surface the nudge, then allow.
106
115
  print(f"[delivery-enforcement advisory] {msg}", file=sys.stderr)
@@ -0,0 +1,119 @@
1
+ """enforcement-self-protect enforcer: the model must not be able to disable
2
+ the delivery gate.
3
+
4
+ A weak local model that hits the delivery-enforcement block tends to try to
5
+ ESCAPE it — observed live (qwen <think>): "set enforcement to advisory mode",
6
+ "just use UAP_DELIVER_BYPASS=1". A single shell `export` does not persist across
7
+ tool calls, so the only way to actually weaken the gate is to PERSIST a bypass:
8
+ write it into a config/env file, or edit the policy/enforcer itself. This
9
+ enforcer blocks exactly those persistence paths.
10
+
11
+ Scope:
12
+ - Edit/Write/MultiEdit to the enforcement control surface (policy md/enforcers,
13
+ .policy-tools/, .uap.json, proxy env, the policy-gate hook scripts).
14
+ - Bash commands that set/export UAP_DELIVER_BYPASS or relax UAP_ENFORCE_DELIVERY,
15
+ write those to a file, or delete/chmod a policy enforcer.
16
+
17
+ There is intentionally NO model-reachable bypass: honoring UAP_DELIVER_BYPASS here
18
+ would reopen the hole (the model would just set it). Operator override is
19
+ out-of-band only: UAP_SELF_PROTECT_OFF=1 in the trusted launch env.
20
+ """
21
+ import os
22
+ import re
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ sys.path.insert(0, str(Path(__file__).parent))
27
+ from _common import emit, parse_cli, repo_root # noqa: E402
28
+
29
+ EDIT_OPS = {"Edit", "Write", "MultiEdit", "edit", "write", "multiedit"}
30
+
31
+ # Path substrings that control delivery enforcement. A write touching any of
32
+ # these can weaken the gate, so it is protected regardless of file extension.
33
+ PROTECTED_MARKERS = (
34
+ "/.policy-tools/",
35
+ "/src/policies/",
36
+ "/policies/", # the policy-definition markdown dir
37
+ "/.uap.json",
38
+ "/.uap/",
39
+ "anthropic-proxy.env",
40
+ "uap-policy-gate.sh",
41
+ "uap-reactor-prompt.sh",
42
+ "pre-tool-use",
43
+ )
44
+ # Normalize leading-slash matching for repo-relative paths.
45
+ PROTECTED_REL = tuple(m.lstrip("/") for m in PROTECTED_MARKERS)
46
+
47
+ # Bash patterns that persist a bypass or relax enforcement.
48
+ BYPASS_PATTERNS = (
49
+ re.compile(r"UAP_DELIVER_BYPASS\s*=\s*['\"]?1", re.I),
50
+ re.compile(r"UAP_ENFORCE_DELIVERY\s*=\s*['\"]?(advisory|off|0|false|no)", re.I),
51
+ re.compile(r"UAP_SELF_PROTECT_OFF\s*=\s*['\"]?1", re.I),
52
+ re.compile(r"UAP_NO_WORKTREE\s*=\s*['\"]?1", re.I),
53
+ re.compile(r"UAP_WORKDIR_SCOPE_OFF\s*=\s*['\"]?1", re.I),
54
+ )
55
+ # Destructive ops against the enforcer/policy surface.
56
+ DESTRUCTIVE = re.compile(
57
+ r"\b(rm|unlink|shred|truncate|mv|chmod)\b[^\n|;&]*"
58
+ r"(\.policy-tools|src/policies|/policies/|anthropic-proxy\.env)",
59
+ re.I,
60
+ )
61
+
62
+ OVERRIDE = os.environ.get("UAP_SELF_PROTECT_OFF") == "1"
63
+
64
+
65
+ def _is_protected_path(rel_posix: str) -> bool:
66
+ low = rel_posix.lower()
67
+ return any(m in ("/" + low) for m in PROTECTED_REL)
68
+
69
+
70
+ def main() -> None:
71
+ if OVERRIDE:
72
+ emit(True, "self-protect disabled by trusted operator override")
73
+
74
+ op, args = parse_cli()
75
+
76
+ if op in EDIT_OPS:
77
+ target = args.get("file_path") or args.get("path") or args.get("target") or ""
78
+ if not target:
79
+ emit(True, "no file path in args")
80
+ try:
81
+ rel = str(Path(target).resolve().relative_to(repo_root()))
82
+ except ValueError:
83
+ # Out-of-repo writes (e.g. the proxy env in ~/.config) — match by name.
84
+ rel = target
85
+ rel_posix = rel.replace(os.sep, "/")
86
+ if _is_protected_path(rel_posix):
87
+ emit(
88
+ False,
89
+ "BLOCKED: this file controls delivery enforcement and cannot be "
90
+ f"edited by the agent ('{rel_posix}'). Do not try to disable or "
91
+ "relax the gate — route your change through the `deliver` tool "
92
+ "instead. (Operator-only override: UAP_SELF_PROTECT_OFF=1.)",
93
+ )
94
+ emit(True, "not an enforcement-control file")
95
+
96
+ if op in {"Bash", "bash"}:
97
+ cmd = args.get("command") or ""
98
+ for pat in BYPASS_PATTERNS:
99
+ if pat.search(cmd):
100
+ emit(
101
+ False,
102
+ "BLOCKED: setting a delivery-enforcement bypass/advisory flag is "
103
+ "not allowed for the agent. Route your change through the "
104
+ "`deliver` tool instead of disabling the gate. "
105
+ "(Operator-only override: UAP_SELF_PROTECT_OFF=1.)",
106
+ )
107
+ if DESTRUCTIVE.search(cmd):
108
+ emit(
109
+ False,
110
+ "BLOCKED: modifying/removing the policy enforcers or proxy env is "
111
+ "not allowed for the agent.",
112
+ )
113
+ emit(True, "no enforcement-tampering in command")
114
+
115
+ emit(True, "not a protected operation")
116
+
117
+
118
+ if __name__ == "__main__":
119
+ main()
@@ -0,0 +1,24 @@
1
+ # Enforcement Self-Protect
2
+
3
+ The delivery gate exists to route coding through `uap deliver`. A weak local
4
+ model that hits the block tends to try to ESCAPE it rather than comply
5
+ (observed: "set enforcement to advisory", "use UAP_DELIVER_BYPASS=1"). This
6
+ policy makes the gate non-disableable by the agent.
7
+
8
+ ## Rules
9
+
10
+ 1. **The control surface is read-only to the agent.** Edits to policy
11
+ definitions, enforcers (`src/policies/`, `.policy-tools/`), `.uap.json`,
12
+ `.uap/`, the proxy env, and the policy-gate hook scripts are blocked.
13
+ 2. **Bypass/relax flags cannot be set by the agent.** Bash commands that set or
14
+ persist `UAP_DELIVER_BYPASS`, relax `UAP_ENFORCE_DELIVERY`, or disable the
15
+ workdir/worktree gates are blocked. (A shell `export` alone does not persist
16
+ across tool calls; this blocks the persistence paths.)
17
+ 3. **No model-reachable bypass.** Honoring a bypass here would reopen the hole.
18
+ Operator override is out-of-band only: `UAP_SELF_PROTECT_OFF=1` in the
19
+ trusted launch environment.
20
+
21
+ ## Rationale
22
+
23
+ Compliance must be the easy path and non-compliance the blocked one. Route
24
+ substantive coding through the `deliver` tool; do not weaken the gate.
@@ -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()
@@ -0,0 +1,71 @@
1
+ """Tests for the enforcement-self-protect enforcer (R2)."""
2
+ import importlib.util
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ ENF = Path(__file__).resolve().parents[3] / "src" / "policies" / "enforcers" / "enforcement_self_protect.py"
10
+ REPO = Path(__file__).resolve().parents[3]
11
+
12
+
13
+ def run(op, args, env=None):
14
+ e = dict(os.environ)
15
+ e["UAP_REPO_ROOT"] = str(REPO)
16
+ e.pop("UAP_SELF_PROTECT_OFF", None)
17
+ if env:
18
+ e.update(env)
19
+ p = subprocess.run(
20
+ [sys.executable, str(ENF), "--operation", op, "--args", json.dumps(args)],
21
+ capture_output=True, text=True, env=e,
22
+ )
23
+ out = json.loads(p.stdout) if p.stdout.strip() else {}
24
+ return p.returncode, out
25
+
26
+
27
+ import unittest
28
+
29
+
30
+ class SelfProtectTest(unittest.TestCase):
31
+ def test_blocks_edit_to_policy_enforcer(self):
32
+ rc, out = run("Write", {"file_path": str(REPO / "src/policies/enforcers/delivery_enforcement.py")})
33
+ self.assertEqual(rc, 2)
34
+ self.assertFalse(out["allowed"])
35
+
36
+ def test_blocks_edit_to_uap_json(self):
37
+ rc, out = run("Edit", {"file_path": str(REPO / ".uap.json")})
38
+ self.assertEqual(rc, 2)
39
+
40
+ def test_blocks_edit_to_policy_tools(self):
41
+ rc, out = run("Write", {"file_path": str(REPO / ".policy-tools/abc_delivery_enforcement.py")})
42
+ self.assertEqual(rc, 2)
43
+
44
+ def test_blocks_bash_setting_bypass(self):
45
+ rc, out = run("Bash", {"command": "export UAP_DELIVER_BYPASS=1 && echo hi"})
46
+ self.assertEqual(rc, 2)
47
+
48
+ def test_blocks_bash_advisory_relax(self):
49
+ rc, out = run("Bash", {"command": "UAP_ENFORCE_DELIVERY=advisory uap deliver x"})
50
+ self.assertEqual(rc, 2)
51
+
52
+ def test_blocks_bash_rm_enforcer(self):
53
+ rc, out = run("Bash", {"command": "rm -f .policy-tools/abc_delivery_enforcement.py"})
54
+ self.assertEqual(rc, 2)
55
+
56
+ def test_allows_normal_source_edit(self):
57
+ rc, out = run("Write", {"file_path": str(REPO / "src/app/index.ts")})
58
+ self.assertEqual(rc, 0)
59
+ self.assertTrue(out["allowed"])
60
+
61
+ def test_allows_normal_bash(self):
62
+ rc, out = run("Bash", {"command": "npm test"})
63
+ self.assertEqual(rc, 0)
64
+
65
+ def test_operator_override_allows(self):
66
+ rc, out = run("Bash", {"command": "export UAP_DELIVER_BYPASS=1"}, env={"UAP_SELF_PROTECT_OFF": "1"})
67
+ self.assertEqual(rc, 0)
68
+
69
+
70
+ if __name__ == "__main__":
71
+ unittest.main()