@miller-tech/uap 1.215.1 → 1.215.2
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 +2 -2
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/enforcement_self_protect.py +45 -0
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/tests/test_conditional_destructive_verbs.py +156 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miller-tech/uap",
|
|
3
|
-
"version": "1.215.
|
|
3
|
+
"version": "1.215.2",
|
|
4
4
|
"description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"start": "node dist/bin/cli.js",
|
|
22
22
|
"test": "vitest",
|
|
23
23
|
"test:ci": "vitest run",
|
|
24
|
-
"test:enforcers": "UAP_PROXY_ENV_AUTOLOAD=0 python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_live_deliver_kill tools.agents.tests.test_schema_diff_gate tools.agents.tests.test_schema_diff_inline tools.agents.tests.test_stream_telemetry tools.agents.tests.test_project_telemetry_events tools.agents.tests.test_workdir_scope_enforcer tools.agents.tests.test_gate_evidence tools.agents.tests.test_repeat_call_guard tools.agents.tests.test_gate_integrity tools.agents.tests.test_gate_failclosed_indirection tools.agents.tests.test_gate_failclosed_schema_diff tools.agents.tests.test_rtk_wrap tools.agents.tests.test_expert_review_pr_scope tools.agents.tests.test_path_containment tools.agents.tests.test_path_normalizer_hardened tools.agents.tests.test_sandbox_tool_strip tools.agents.tests.test_proxy_env_loader tools.agents.tests.test_stream_required_tool tools.agents.tests.test_tool_call_wrapper_profiles tools.agents.tests.test_tool_convert_cache tools.agents.tests.test_doubling_break tools.agents.tests.test_error_loop_ignores_correctives tools.agents.tests.test_attractor_detection tools.agents.tests.test_client_disconnect tools.agents.tests.test_confidence_escalation tools.agents.tests.test_coordination_ban tools.agents.tests.test_coordination_early_ban tools.agents.tests.test_cycle_break_exploration tools.agents.tests.test_deferral_break tools.agents.tests.test_deliver_autoroute tools.agents.tests.test_delivery_enforcement_all_langs tools.agents.tests.test_delivery_enforcement_exemptions tools.agents.tests.test_delivery_enforcement_filepath tools.agents.tests.test_delivery_enforcement_write_complexity tools.agents.tests.test_fastpath_config tools.agents.tests.test_delivery_enforcement_web_and_bash tools.agents.tests.test_disconnect_watcher tools.agents.tests.test_empty_maxtokens_recovery tools.agents.tests.test_empty_tool_loop_break tools.agents.tests.test_enforcer_escape_hatches tools.agents.tests.test_error_loop_break tools.agents.tests.test_finalize_suppression tools.agents.tests.test_malformed_unclosed_think tools.agents.tests.test_mandate_beats_recon tools.agents.tests.test_mandate_deliver tools.agents.tests.test_overflow_truncate_count_tokens tools.agents.tests.test_passthrough_oauth tools.agents.tests.test_project_telemetry tools.agents.tests.test_proxy_auth_headers tools.agents.tests.test_prune_preserve_force_write tools.agents.tests.test_recon_deliver_gate tools.agents.tests.test_session_admission tools.agents.tests.test_stream_heartbeat tools.agents.tests.test_stuck_break_reattach tools.agents.tests.test_turn_count_breaker_periodic tools.agents.tests.test_upstream_chokepoint tools.agents.tests.test_vision_passthrough tools.agents.tests.test_worktree_required tools.agents.tests.test_enforcer_suite_coverage tools.agents.tests.test_validate_plan_gate tools.agents.tests.test_validate_plan_inside_project tools.agents.tests.test_anthropic_proxy_streaming tools.agents.tests.test_non_stream_resp_log tools.agents.tests.test_delivery_enforcement_worktree tools.agents.tests.test_output_token_ceilings tools.agents.tests.test_tool_narrowing_core tools.agents.tests.test_models_context_window tools.agents.tests.test_thinking_template_kwargs tools.agents.tests.test_cycle_break_wait_poll",
|
|
24
|
+
"test:enforcers": "UAP_PROXY_ENV_AUTOLOAD=0 python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_conditional_destructive_verbs tools.agents.tests.test_live_deliver_kill tools.agents.tests.test_schema_diff_gate tools.agents.tests.test_schema_diff_inline tools.agents.tests.test_stream_telemetry tools.agents.tests.test_project_telemetry_events tools.agents.tests.test_workdir_scope_enforcer tools.agents.tests.test_gate_evidence tools.agents.tests.test_repeat_call_guard tools.agents.tests.test_gate_integrity tools.agents.tests.test_gate_failclosed_indirection tools.agents.tests.test_gate_failclosed_schema_diff tools.agents.tests.test_rtk_wrap tools.agents.tests.test_expert_review_pr_scope tools.agents.tests.test_path_containment tools.agents.tests.test_path_normalizer_hardened tools.agents.tests.test_sandbox_tool_strip tools.agents.tests.test_proxy_env_loader tools.agents.tests.test_stream_required_tool tools.agents.tests.test_tool_call_wrapper_profiles tools.agents.tests.test_tool_convert_cache tools.agents.tests.test_doubling_break tools.agents.tests.test_error_loop_ignores_correctives tools.agents.tests.test_attractor_detection tools.agents.tests.test_client_disconnect tools.agents.tests.test_confidence_escalation tools.agents.tests.test_coordination_ban tools.agents.tests.test_coordination_early_ban tools.agents.tests.test_cycle_break_exploration tools.agents.tests.test_deferral_break tools.agents.tests.test_deliver_autoroute tools.agents.tests.test_delivery_enforcement_all_langs tools.agents.tests.test_delivery_enforcement_exemptions tools.agents.tests.test_delivery_enforcement_filepath tools.agents.tests.test_delivery_enforcement_write_complexity tools.agents.tests.test_fastpath_config tools.agents.tests.test_delivery_enforcement_web_and_bash tools.agents.tests.test_disconnect_watcher tools.agents.tests.test_empty_maxtokens_recovery tools.agents.tests.test_empty_tool_loop_break tools.agents.tests.test_enforcer_escape_hatches tools.agents.tests.test_error_loop_break tools.agents.tests.test_finalize_suppression tools.agents.tests.test_malformed_unclosed_think tools.agents.tests.test_mandate_beats_recon tools.agents.tests.test_mandate_deliver tools.agents.tests.test_overflow_truncate_count_tokens tools.agents.tests.test_passthrough_oauth tools.agents.tests.test_project_telemetry tools.agents.tests.test_proxy_auth_headers tools.agents.tests.test_prune_preserve_force_write tools.agents.tests.test_recon_deliver_gate tools.agents.tests.test_session_admission tools.agents.tests.test_stream_heartbeat tools.agents.tests.test_stuck_break_reattach tools.agents.tests.test_turn_count_breaker_periodic tools.agents.tests.test_upstream_chokepoint tools.agents.tests.test_vision_passthrough tools.agents.tests.test_worktree_required tools.agents.tests.test_enforcer_suite_coverage tools.agents.tests.test_validate_plan_gate tools.agents.tests.test_validate_plan_inside_project tools.agents.tests.test_anthropic_proxy_streaming tools.agents.tests.test_non_stream_resp_log tools.agents.tests.test_delivery_enforcement_worktree tools.agents.tests.test_output_token_ceilings tools.agents.tests.test_tool_narrowing_core tools.agents.tests.test_models_context_window tools.agents.tests.test_thinking_template_kwargs tools.agents.tests.test_cycle_break_wait_poll",
|
|
25
25
|
"test:coverage": "vitest --coverage",
|
|
26
26
|
"bench": "vitest --config vitest.bench.config.ts",
|
|
27
27
|
"lint": "eslint src --ext .ts",
|
|
Binary file
|
|
@@ -127,6 +127,48 @@ DESTRUCTIVE_VERBS = (
|
|
|
127
127
|
"dd", "cp", "tee", "install", "sed", "find", "ln",
|
|
128
128
|
)
|
|
129
129
|
|
|
130
|
+
|
|
131
|
+
# Verbs from that list that only mutate under a FLAG. Without one they are
|
|
132
|
+
# stdout filters, exactly like `awk` and `grep` -- which this enforcer already
|
|
133
|
+
# allows against a protected path, because READING one is not tampering.
|
|
134
|
+
#
|
|
135
|
+
# Measured on this enforcer (2026-08-20): of six read-only ways to inspect a
|
|
136
|
+
# protected file, five were refused -- `sed -n '1,5p'`, `sed 's/a/b/'`, the same
|
|
137
|
+
# piped to `wc`, `find <dir> -name '*.py'` and `find <dir> -type f` -- while
|
|
138
|
+
# `cat`, `grep`, `head`, `wc` and `awk` were allowed. Those refusals protected
|
|
139
|
+
# nothing and cost real work: in one session they blocked READING this file
|
|
140
|
+
# twice, blocked `cp <protected> /tmp/` (a read FROM a protected path), and
|
|
141
|
+
# blocked a `gh pr close --comment` whose comment text merely quoted a path.
|
|
142
|
+
#
|
|
143
|
+
# Each predicate answers one question -- "does THIS invocation write?" -- and
|
|
144
|
+
# fails CLOSED: an unrecognised short-flag cluster containing `i` counts as a
|
|
145
|
+
# write, and any -exec-family action counts as a write, because what it runs
|
|
146
|
+
# cannot be judged from the flag alone. A verb NOT listed here is unchanged:
|
|
147
|
+
# still unconditionally destructive.
|
|
148
|
+
def _sed_writes(tokens) -> bool:
|
|
149
|
+
"""GNU sed mutates only with -i / --in-place (optionally suffixed)."""
|
|
150
|
+
for t in tokens:
|
|
151
|
+
if t.startswith("--"):
|
|
152
|
+
if t.startswith("--in-place"):
|
|
153
|
+
return True
|
|
154
|
+
continue
|
|
155
|
+
if t.startswith("-") and len(t) > 1:
|
|
156
|
+
# Short-flag cluster: -i, -i.bak, -ni, -Ei all carry the write.
|
|
157
|
+
# Anything after the first '.' is the backup suffix, not flags.
|
|
158
|
+
if "i" in t[1:].split(".", 1)[0]:
|
|
159
|
+
return True
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _find_writes(tokens) -> bool:
|
|
164
|
+
"""`find` mutates via -delete, an -exec-family action, or an -f* writer."""
|
|
165
|
+
mutating = {"-delete", "-exec", "-execdir", "-ok", "-okdir",
|
|
166
|
+
"-fprint", "-fprint0", "-fprintf", "-fls"}
|
|
167
|
+
return any(t in mutating for t in tokens)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
CONDITIONAL_VERBS = {"sed": _sed_writes, "find": _find_writes}
|
|
171
|
+
|
|
130
172
|
# Paths inside a protected tree that the agent legitimately writes. Without
|
|
131
173
|
# these the enforcer deadlocks the workflows it exists to support: the
|
|
132
174
|
# expert-review gate REQUIRES writing .uap/reviews/<branch>.json, and the visual
|
|
@@ -518,6 +560,9 @@ def _direct_destructive(command: str) -> bool:
|
|
|
518
560
|
continue
|
|
519
561
|
elif verb not in DESTRUCTIVE_VERBS:
|
|
520
562
|
continue
|
|
563
|
+
# A conditionally-destructive verb without its write flag is a READ.
|
|
564
|
+
elif verb in CONDITIONAL_VERBS and not CONDITIONAL_VERBS[verb](tokens[1:]):
|
|
565
|
+
continue
|
|
521
566
|
if cd_into_protected or _mentions_protected(" ".join(tokens[1:])):
|
|
522
567
|
return True
|
|
523
568
|
return False
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""`sed` and `find` are only destructive when a flag makes them write.
|
|
2
|
+
|
|
3
|
+
Both sit in DESTRUCTIVE_VERBS, but without a write flag they are stdout filters
|
|
4
|
+
-- exactly like `awk` and `grep`, which this enforcer already allows against a
|
|
5
|
+
protected path, because READING one is not tampering.
|
|
6
|
+
|
|
7
|
+
Measured before the fix (2026-08-20): of six read-only ways to inspect a
|
|
8
|
+
protected file, five were refused -- `sed -n '1,5p'`, `sed 's/a/b/'`, the same
|
|
9
|
+
piped to `wc`, `find <dir> -name '*.py'` and `find <dir> -type f`. Those refusals
|
|
10
|
+
protected nothing and cost real work: in one session they blocked READING the
|
|
11
|
+
enforcer twice, blocked `cp <protected> /tmp/` (a read FROM a protected path),
|
|
12
|
+
and blocked a `gh pr close --comment` whose comment text merely quoted a path.
|
|
13
|
+
|
|
14
|
+
The predicates fail CLOSED, so the second class below matters more than the
|
|
15
|
+
first: every way of making `sed` or `find` write must still be refused.
|
|
16
|
+
"""
|
|
17
|
+
import importlib.util
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import unittest
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
ENF = Path(__file__).resolve().parents[3] / "src" / "policies" / "enforcers" / "enforcement_self_protect.py"
|
|
26
|
+
REPO = Path(__file__).resolve().parents[3]
|
|
27
|
+
T = "src/policies/enforcers/enforcement_self_protect.py"
|
|
28
|
+
D = "src/policies/enforcers"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def blocked(cmd):
|
|
32
|
+
e = dict(os.environ)
|
|
33
|
+
e["UAP_REPO_ROOT"] = str(REPO)
|
|
34
|
+
e.pop("UAP_SELF_PROTECT_OFF", None)
|
|
35
|
+
p = subprocess.run(
|
|
36
|
+
[sys.executable, str(ENF), "--operation", "Bash", "--args", json.dumps({"command": cmd})],
|
|
37
|
+
capture_output=True, text=True, env=e,
|
|
38
|
+
)
|
|
39
|
+
return p.returncode != 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ReadsAreAllowedTest(unittest.TestCase):
|
|
43
|
+
"""A read of a protected file is not tampering, whatever tool does it."""
|
|
44
|
+
|
|
45
|
+
def test_sed_without_in_place_is_a_read(self):
|
|
46
|
+
for cmd in (
|
|
47
|
+
"sed -n '1,5p' " + T,
|
|
48
|
+
"sed 's/a/b/' " + T,
|
|
49
|
+
"sed -E 's/a+/b/' " + T,
|
|
50
|
+
"sed -n '1,5p' " + T + " | wc -l",
|
|
51
|
+
):
|
|
52
|
+
with self.subTest(cmd=cmd):
|
|
53
|
+
self.assertFalse(blocked(cmd), cmd)
|
|
54
|
+
|
|
55
|
+
def test_find_without_a_mutating_action_is_a_read(self):
|
|
56
|
+
for cmd in ("find " + D + " -name '*.py'", "find " + D + " -type f"):
|
|
57
|
+
with self.subTest(cmd=cmd):
|
|
58
|
+
self.assertFalse(blocked(cmd), cmd)
|
|
59
|
+
|
|
60
|
+
def test_the_readers_that_always_worked_still_work(self):
|
|
61
|
+
# Controls. If these ever start blocking, the regression is upstream of
|
|
62
|
+
# the conditional-verb logic, not in it.
|
|
63
|
+
for cmd in ("cat " + T, "grep -n x " + T, "awk 'NR<5' " + T, "head -5 " + T):
|
|
64
|
+
with self.subTest(cmd=cmd):
|
|
65
|
+
self.assertFalse(blocked(cmd), cmd)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class WritesStayRefusedTest(unittest.TestCase):
|
|
69
|
+
"""Every way of making these two verbs write must still be refused."""
|
|
70
|
+
|
|
71
|
+
def test_sed_in_place_in_all_its_spellings(self):
|
|
72
|
+
for cmd in (
|
|
73
|
+
"sed -i 's/a/b/' " + T,
|
|
74
|
+
"sed --in-place 's/a/b/' " + T,
|
|
75
|
+
"sed -i.bak 's/a/b/' " + T, # suffixed backup
|
|
76
|
+
"sed -ni 's/a/b/' " + T, # bundled short flags
|
|
77
|
+
"sed -Ei 's/a/b/' " + T, # bundled with -E
|
|
78
|
+
"sed --in-place=.bak 's/a/b/' " + T,
|
|
79
|
+
):
|
|
80
|
+
with self.subTest(cmd=cmd):
|
|
81
|
+
self.assertTrue(blocked(cmd), cmd)
|
|
82
|
+
|
|
83
|
+
def test_find_mutating_actions(self):
|
|
84
|
+
for cmd in (
|
|
85
|
+
"find " + D + " -name '*.py' -delete",
|
|
86
|
+
"find " + D + " -name '*.py' -exec rm {} ;",
|
|
87
|
+
"find " + D + " -execdir rm {} ;",
|
|
88
|
+
"find " + D + " -ok rm {} ;",
|
|
89
|
+
"find " + D + " -fprintf " + T + " %p",
|
|
90
|
+
):
|
|
91
|
+
with self.subTest(cmd=cmd):
|
|
92
|
+
self.assertTrue(blocked(cmd), cmd)
|
|
93
|
+
|
|
94
|
+
def test_unconditional_verbs_are_untouched(self):
|
|
95
|
+
# A verb NOT in CONDITIONAL_VERBS keeps its old, flag-independent
|
|
96
|
+
# treatment. This is the guard against the fix widening past its scope.
|
|
97
|
+
for cmd in (
|
|
98
|
+
"rm " + T,
|
|
99
|
+
"mv " + T + " /tmp/x",
|
|
100
|
+
"cp /tmp/x " + T,
|
|
101
|
+
"echo x | tee " + T,
|
|
102
|
+
"echo x > " + T,
|
|
103
|
+
"chmod 000 " + T,
|
|
104
|
+
"truncate -s 0 " + T,
|
|
105
|
+
"shred " + T,
|
|
106
|
+
"ln -sf /tmp/x " + T,
|
|
107
|
+
):
|
|
108
|
+
with self.subTest(cmd=cmd):
|
|
109
|
+
self.assertTrue(blocked(cmd), cmd)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class PredicatesFailClosedTest(unittest.TestCase):
|
|
113
|
+
"""Unit-level: an unrecognised flag must read as a WRITE, not a read."""
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def setUpClass(cls):
|
|
117
|
+
spec = importlib.util.spec_from_file_location("esp", ENF)
|
|
118
|
+
cls.m = importlib.util.module_from_spec(spec)
|
|
119
|
+
sys.modules["esp"] = cls.m
|
|
120
|
+
spec.loader.exec_module(cls.m)
|
|
121
|
+
|
|
122
|
+
def test_sed_short_flag_cluster_containing_i_is_a_write(self):
|
|
123
|
+
for toks in (["-i"], ["-i.bak"], ["-ni"], ["-Ei"], ["-i", "s/a/b/"]):
|
|
124
|
+
with self.subTest(toks=toks):
|
|
125
|
+
self.assertTrue(self.m._sed_writes(toks), toks)
|
|
126
|
+
|
|
127
|
+
def test_sed_flags_without_i_are_reads(self):
|
|
128
|
+
for toks in (["-n"], ["-E"], ["-r"], ["-e", "s/a/b/"], ["--expression=s/a/b/"]):
|
|
129
|
+
with self.subTest(toks=toks):
|
|
130
|
+
self.assertFalse(self.m._sed_writes(toks), toks)
|
|
131
|
+
|
|
132
|
+
def test_sed_backup_suffix_is_not_parsed_as_flags(self):
|
|
133
|
+
# `-n.i` would otherwise look like a write because of the trailing i;
|
|
134
|
+
# everything after the first '.' is the suffix, not a flag cluster.
|
|
135
|
+
self.assertFalse(self.m._sed_writes(["-n.i"]))
|
|
136
|
+
|
|
137
|
+
def test_find_exec_family_is_a_write_whatever_it_runs(self):
|
|
138
|
+
# What -exec runs cannot be judged from the flag, so it counts as a write.
|
|
139
|
+
for tok in ("-delete", "-exec", "-execdir", "-ok", "-okdir",
|
|
140
|
+
"-fprint", "-fprint0", "-fprintf", "-fls"):
|
|
141
|
+
with self.subTest(tok=tok):
|
|
142
|
+
self.assertTrue(self.m._find_writes([tok]))
|
|
143
|
+
|
|
144
|
+
def test_find_pure_predicates_are_reads(self):
|
|
145
|
+
for toks in (["-name", "*.py"], ["-type", "f"], ["-maxdepth", "2"], ["-print"]):
|
|
146
|
+
with self.subTest(toks=toks):
|
|
147
|
+
self.assertFalse(self.m._find_writes(toks), toks)
|
|
148
|
+
|
|
149
|
+
def test_only_these_two_verbs_are_conditional(self):
|
|
150
|
+
# Widening this map is a security decision; pin the membership so it
|
|
151
|
+
# cannot grow by accident.
|
|
152
|
+
self.assertEqual(set(self.m.CONDITIONAL_VERBS), {"sed", "find"})
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if __name__ == "__main__":
|
|
156
|
+
unittest.main()
|