@miller-tech/uap 2.21.1 → 2.21.3

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": "2.21.1",
3
+ "version": "2.21.3",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -734,7 +734,15 @@ def main() -> None:
734
734
  op, args = parse_cli()
735
735
 
736
736
  if op in EDIT_OPS:
737
- target = args.get("file_path") or args.get("path") or args.get("target") or ""
737
+ # Key list widened for opencode, which sends camelCase `filePath`.
738
+ # It previously landed on the fail-open "no file path in args" branch,
739
+ # so agent edits to protected files (.uap.json, enforcers, hooks) were
740
+ # allowed whenever the caller used a different key convention — the
741
+ # gate armed its fail-closed net, then this enforcer blinked first and
742
+ # allowed. Widening only ever BLOCKS: every added key names a file the
743
+ # agent is editing, not one it is merely reading.
744
+ target = (args.get("file_path") or args.get("filePath") or args.get("path")
745
+ or args.get("target") or args.get("filename") or args.get("file") or "")
738
746
  if not target:
739
747
  emit(True, "no file path in args")
740
748
  rp = Path(target).resolve()
@@ -252,7 +252,14 @@ except Exception: a = {}
252
252
  markers = ("/.policy-tools/", "/src/policies/", "/policies/", "/.uap.json",
253
253
  ".uap.json", "/.uap/", "anthropic-proxy.env", "uap-policy-gate.sh",
254
254
  "uap-reactor-prompt.sh", "pre-tool-use")
255
- target = a.get("file_path") or a.get("path") or a.get("target") or ""
255
+ # Key list matches the one at the top of this script (worktree resolution):
256
+ # harnesses disagree on the arg name (file_path vs filePath vs target/…), and
257
+ # an op whose path key this list misses scores SEC_SENSITIVE=0 — arming
258
+ # neither the fail-closed net nor the fastpath skip. Observed live: opencode
259
+ # sends filePath (camelCase), so a trivial agent edit to .uap.json was
260
+ # fast-pathed around the self-protect enforcer. Widening only TIGHTENS.
261
+ target = (a.get("file_path") or a.get("filePath") or a.get("path")
262
+ or a.get("target") or a.get("filename") or a.get("file") or "")
256
263
  cmd = a.get("command") or ""
257
264
  # Scan the COMMAND as well as file_path. Only file_path was checked, so for
258
265
  # every Bash call SEC_SENSITIVE was 0 unless the command set a bypass var --
@@ -37,6 +37,25 @@ class SelfProtectTest(unittest.TestCase):
37
37
  rc, out = run("Edit", {"file_path": str(REPO / ".uap.json")})
38
38
  self.assertEqual(rc, 2)
39
39
 
40
+ def test_blocks_edit_to_uap_json_camel_case(self):
41
+ # opencode sends camelCase `filePath`. The extraction previously read
42
+ # only file_path/path/target, so this shape landed on the fail-open
43
+ # "no file path in args" branch and the edit was ALLOWED — observed
44
+ # live: a looping client repeatedly tried to strip a policy flag from
45
+ # .uap.json and every attempt sailed past this enforcer.
46
+ rc, out = run("Edit", {"filePath": str(REPO / ".uap.json"), "oldString": "x", "newString": "y"})
47
+ self.assertEqual(rc, 2)
48
+ self.assertFalse(out["allowed"])
49
+ self.assertIn("BLOCKED", out["reason"])
50
+
51
+ def test_allows_normal_file_edit_camel_case(self):
52
+ # The widened key list must not over-block: an ordinary file named with
53
+ # the same convention still allows, with the same reason as before.
54
+ rc, out = run("Edit", {"filePath": str(REPO / "README.md"), "oldString": "x", "newString": "y"})
55
+ self.assertEqual(rc, 0)
56
+ self.assertTrue(out["allowed"])
57
+ self.assertEqual(out["reason"], "not an enforcement-control file")
58
+
40
59
  def test_blocks_edit_to_policy_tools(self):
41
60
  rc, out = run("Write", {"file_path": str(REPO / ".policy-tools/abc_delivery_enforcement.py")})
42
61
  self.assertEqual(rc, 2)