@miller-tech/uap 1.196.0 → 1.197.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.
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env python3
2
+ """How big a "trivial" edit is, is a project decision — so it belongs in the repo.
3
+
4
+ The fast-path budgets (per-edit threshold, cumulative chars, cumulative edits)
5
+ were read from the environment only. That meant only whoever launched the agent
6
+ could size them, and the choice left no trace: nothing in the repo recorded that
7
+ this project had decided a bigger budget, so the next session silently got the
8
+ default again.
9
+
10
+ They now come from `.uap.json` (`delivery.trivialEditChars`,
11
+ `delivery.cumulativeChars`, `delivery.cumulativeEdits`), with the environment
12
+ still winning where it is set, so a one-session operator override still works.
13
+
14
+ Both halves of the gate read the SAME setting: the hook that decides fast-path,
15
+ and the enforcer that decides refusal. Two numbers for one question would mean
16
+ the hook fast-paths an edit the enforcer then refuses.
17
+ """
18
+
19
+ import importlib.util
20
+ import json
21
+ import os
22
+ import subprocess
23
+ import sys
24
+ import tempfile
25
+ import unittest
26
+ from pathlib import Path
27
+
28
+ ROOT = Path(__file__).resolve().parents[3]
29
+ HOOK = ROOT / "templates" / "hooks" / "fastpath_gate.py"
30
+
31
+
32
+ def _load(path: Path, name: str):
33
+ spec = importlib.util.spec_from_file_location(name, path)
34
+ assert spec and spec.loader
35
+ m = importlib.util.module_from_spec(spec)
36
+ spec.loader.exec_module(m)
37
+ return m
38
+
39
+
40
+ hook = _load(HOOK, "fastpath_gate")
41
+ enf = _load(ROOT / "src" / "policies" / "enforcers" / "delivery_enforcement.py", "denf")
42
+
43
+
44
+ class Project:
45
+ def __init__(self, delivery: dict | None = None, raw: str | None = None):
46
+ self.tmp = tempfile.TemporaryDirectory()
47
+ self.root = Path(self.tmp.name)
48
+ if raw is not None:
49
+ (self.root / ".uap.json").write_text(raw)
50
+ elif delivery is not None:
51
+ (self.root / ".uap.json").write_text(json.dumps({"version": 1, "delivery": delivery}))
52
+
53
+ def close(self):
54
+ self.tmp.cleanup()
55
+
56
+
57
+ class HookReadsTheProjectConfig(unittest.TestCase):
58
+ def setUp(self):
59
+ self.saved = {k: os.environ.get(k) for k in ("TRIVIAL", "CUM_CHARS", "CUM_EDITS", "UAP_MAIN_ROOT")}
60
+ for k in self.saved:
61
+ os.environ.pop(k, None)
62
+
63
+ def tearDown(self):
64
+ for k, v in self.saved.items():
65
+ if v is None:
66
+ os.environ.pop(k, None)
67
+ else:
68
+ os.environ[k] = v
69
+
70
+ def test_uses_the_documented_default_when_nothing_is_configured(self):
71
+ p = Project()
72
+ os.environ["UAP_MAIN_ROOT"] = str(p.root)
73
+ self.assertEqual(hook._configured("trivialEditChars", "TRIVIAL", 240), 240)
74
+ self.assertEqual(hook._configured("cumulativeChars", "CUM_CHARS", 800), 800)
75
+ p.close()
76
+
77
+ def test_reads_the_value_committed_in_uap_json(self):
78
+ p = Project({"trivialEditChars": 800, "cumulativeChars": 6000})
79
+ os.environ["UAP_MAIN_ROOT"] = str(p.root)
80
+ self.assertEqual(hook._configured("trivialEditChars", "TRIVIAL", 240), 800)
81
+ self.assertEqual(hook._configured("cumulativeChars", "CUM_CHARS", 800), 6000)
82
+ p.close()
83
+
84
+ def test_the_environment_still_wins_over_the_project(self):
85
+ # An operator sizing one session must not have to edit the repo.
86
+ p = Project({"trivialEditChars": 800})
87
+ os.environ["UAP_MAIN_ROOT"] = str(p.root)
88
+ os.environ["TRIVIAL"] = "50"
89
+ self.assertEqual(hook._configured("trivialEditChars", "TRIVIAL", 240), 50)
90
+ p.close()
91
+
92
+ def test_a_malformed_override_falls_through_instead_of_crashing(self):
93
+ p = Project({"trivialEditChars": 800})
94
+ os.environ["UAP_MAIN_ROOT"] = str(p.root)
95
+ os.environ["TRIVIAL"] = "not-a-number"
96
+ self.assertEqual(hook._configured("trivialEditChars", "TRIVIAL", 240), 800)
97
+ p.close()
98
+
99
+ def test_a_broken_config_does_not_break_the_gate(self):
100
+ # A gate that throws on a malformed config is a gate that blocks all work.
101
+ for raw in ("{not json", "[]", '{"delivery": "nope"}', '{"delivery": {"trivialEditChars": "big"}}'):
102
+ p = Project(raw=raw)
103
+ os.environ["UAP_MAIN_ROOT"] = str(p.root)
104
+ self.assertEqual(hook._configured("trivialEditChars", "TRIVIAL", 240), 240, raw)
105
+ p.close()
106
+
107
+ def test_a_boolean_is_not_a_budget(self):
108
+ # True would otherwise arrive as 1, making every edit non-trivial.
109
+ p = Project({"trivialEditChars": True})
110
+ os.environ["UAP_MAIN_ROOT"] = str(p.root)
111
+ self.assertEqual(hook._configured("trivialEditChars", "TRIVIAL", 240), 240)
112
+ p.close()
113
+
114
+ def test_a_negative_budget_clamps_rather_than_inverting_the_gate(self):
115
+ p = Project({"cumulativeChars": -5})
116
+ os.environ["UAP_MAIN_ROOT"] = str(p.root)
117
+ self.assertEqual(hook._configured("cumulativeChars", "CUM_CHARS", 800), 0)
118
+ p.close()
119
+
120
+ def test_a_missing_config_file_is_normal_not_an_error(self):
121
+ p = Project()
122
+ os.environ["UAP_MAIN_ROOT"] = str(p.root)
123
+ self.assertEqual(hook._configured("cumulativeEdits", "CUM_EDITS", 6), 6)
124
+ p.close()
125
+
126
+
127
+ class BothHalvesAgree(unittest.TestCase):
128
+ """The hook decides fast-path; the enforcer decides refusal. One number."""
129
+
130
+ def setUp(self):
131
+ self.saved = {k: os.environ.get(k) for k in
132
+ ("TRIVIAL", "UAP_DELIVER_TRIVIAL_EDIT_CHARS", "UAP_MAIN_ROOT")}
133
+ for k in self.saved:
134
+ os.environ.pop(k, None)
135
+
136
+ def tearDown(self):
137
+ for k, v in self.saved.items():
138
+ if v is None:
139
+ os.environ.pop(k, None)
140
+ else:
141
+ os.environ[k] = v
142
+
143
+ def test_enforcer_and_hook_read_the_same_project_value(self):
144
+ p = Project({"trivialEditChars": 900})
145
+ os.environ["UAP_MAIN_ROOT"] = str(p.root)
146
+ self.assertEqual(enf._trivial_edit_chars(), 900)
147
+ self.assertEqual(hook._configured("trivialEditChars", "TRIVIAL", 240), 900)
148
+ p.close()
149
+
150
+ def test_enforcer_honours_its_own_env_override_first(self):
151
+ p = Project({"trivialEditChars": 900})
152
+ os.environ["UAP_MAIN_ROOT"] = str(p.root)
153
+ os.environ["UAP_DELIVER_TRIVIAL_EDIT_CHARS"] = "120"
154
+ self.assertEqual(enf._trivial_edit_chars(), 120)
155
+ p.close()
156
+
157
+ def test_enforcer_falls_back_to_the_same_default(self):
158
+ p = Project()
159
+ os.environ["UAP_MAIN_ROOT"] = str(p.root)
160
+ self.assertEqual(enf._trivial_edit_chars(), 240)
161
+ p.close()
162
+
163
+
164
+ class TheDecisionActuallyUsesIt(unittest.TestCase):
165
+ """Testing the reader is not testing the gate.
166
+
167
+ `_configured` can be perfect while the call sites still read the raw
168
+ environment — the decision is what ships, so drive the real entry point.
169
+ """
170
+
171
+ def _decide(self, root: Path, payload: dict, env: dict) -> int:
172
+ e = dict(os.environ)
173
+ for k in ("TRIVIAL", "CUM_CHARS", "CUM_EDITS"):
174
+ e.pop(k, None)
175
+ e["UAP_MAIN_ROOT"] = str(root)
176
+ e.update(env)
177
+ r = subprocess.run([sys.executable, str(HOOK)], input=json.dumps(payload),
178
+ capture_output=True, text=True, cwd=str(root), env=e)
179
+ return r.returncode
180
+
181
+ def test_a_project_sized_threshold_changes_the_verdict(self):
182
+ # 400 changed chars: routed under the 240 default, fast-pathed under a
183
+ # project that has decided 800.
184
+ edit = {"file_path": "src/app.ts", "old_string": "x" * 200, "new_string": "y" * 200}
185
+ strict = Project({"trivialEditChars": 240, "cumulativeChars": 100000})
186
+ loose = Project({"trivialEditChars": 800, "cumulativeChars": 100000})
187
+ try:
188
+ self.assertEqual(self._decide(strict.root, edit, {}), 1, "should ROUTE under 240")
189
+ self.assertEqual(self._decide(loose.root, edit, {}), 0, "should FAST-PATH under 800")
190
+ finally:
191
+ strict.close()
192
+ loose.close()
193
+
194
+ def test_a_project_sized_cumulative_budget_changes_the_verdict(self):
195
+ # Same small edit repeated: the cumulative budget decides when it routes.
196
+ edit = {"file_path": "src/app.ts", "old_string": "x" * 50, "new_string": "y" * 50}
197
+ tight = Project({"trivialEditChars": 240, "cumulativeChars": 150, "cumulativeEdits": 99})
198
+ roomy = Project({"trivialEditChars": 240, "cumulativeChars": 100000, "cumulativeEdits": 99})
199
+ try:
200
+ self.assertEqual(self._decide(tight.root, edit, {}), 0, "first edit fits")
201
+ self.assertEqual(self._decide(tight.root, edit, {}), 1, "second crosses 150 chars")
202
+ self.assertEqual(self._decide(roomy.root, edit, {}), 0)
203
+ self.assertEqual(self._decide(roomy.root, edit, {}), 0, "still inside a big budget")
204
+ finally:
205
+ tight.close()
206
+ roomy.close()
207
+
208
+ def test_the_environment_still_overrides_the_project_end_to_end(self):
209
+ edit = {"file_path": "src/app.ts", "old_string": "x" * 200, "new_string": "y" * 200}
210
+ p = Project({"trivialEditChars": 800, "cumulativeChars": 100000})
211
+ try:
212
+ self.assertEqual(self._decide(p.root, edit, {}), 0)
213
+ self.assertEqual(self._decide(p.root, edit, {"TRIVIAL": "100"}), 1)
214
+ finally:
215
+ p.close()
216
+
217
+
218
+ class EveryInstalledCopyCarriesIt(unittest.TestCase):
219
+ """Hook template drift: a fix that never reaches templates/ is reverted the
220
+ next time a worktree is created, and a fix only in templates/ never runs."""
221
+
222
+ def test_all_copies_read_the_config(self):
223
+ # Filter on the path RELATIVE to the root: this test may itself be
224
+ # running inside .worktrees/, so matching the absolute path excluded
225
+ # every copy and the assertion silently passed on an empty list.
226
+ copies = []
227
+ for p in ROOT.rglob("fastpath_gate.py"):
228
+ rel = str(p.relative_to(ROOT))
229
+ if "node_modules" in rel or rel.startswith(".worktrees"):
230
+ continue
231
+ copies.append(p)
232
+ self.assertGreater(len(copies), 1, "expected a template plus installed copies")
233
+ for p in copies:
234
+ self.assertIn("_configured(", p.read_text(), f"{p} still reads env only")
235
+
236
+
237
+ if __name__ == "__main__":
238
+ unittest.main()
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env python3
2
+ """Killing a RUNNING deliver run is the blocker, not planning.
3
+
4
+ Measured 2026-08-11: three consecutive runs reached turn 3, turn 8 and turn 10
5
+ and every one was terminated from outside. The proxy journal caught the shape —
6
+ a kill of the run's own pid, followed by a cooperative stop request, five
7
+ minutes after launch. Both halves are the caller trying to stop a run; only the
8
+ second keeps the work. The first drops the turn in flight with the lock still
9
+ held, and it is why "deliver never gets past planning" was believed while the
10
+ runs were in fact working.
11
+
12
+ The rule has to be NARROW: killing anything else stays allowed, because a
13
+ blanket refusal on `kill` would block ordinary process cleanup for no gain.
14
+ """
15
+
16
+ import importlib.util
17
+ import json
18
+ import os
19
+ import subprocess
20
+ import sys
21
+ import tempfile
22
+ import time
23
+ import unittest
24
+ from pathlib import Path
25
+
26
+
27
+ def _load():
28
+ p = Path(__file__).resolve().parents[3] / "src" / "policies" / "enforcers" / "enforcement_self_protect.py"
29
+ spec = importlib.util.spec_from_file_location("esp", p)
30
+ assert spec and spec.loader
31
+ m = importlib.util.module_from_spec(spec)
32
+ spec.loader.exec_module(m)
33
+ return m
34
+
35
+
36
+ esp = _load()
37
+
38
+
39
+ class LiveRunFixture:
40
+ """A real sleeping process, recorded as a running deliver run."""
41
+
42
+ def __init__(self, root: Path, run_id: str = "run-20260811T091247-84d1f1", pid: int | None = None):
43
+ self.root = root
44
+ self.run_id = run_id
45
+ self.proc = None
46
+ if pid is None:
47
+ # argv must contain "deliver" — the rule verifies against /proc so a
48
+ # recycled pid cannot be mistaken for a run.
49
+ self.proc = subprocess.Popen(
50
+ [sys.executable, "-c", "import time,sys; sys.argv.append('deliver'); time.sleep(30)", "deliver"]
51
+ )
52
+ pid = self.proc.pid
53
+ self.pid = pid
54
+ d = root / ".uap" / "deliver-runs" / run_id
55
+ d.mkdir(parents=True, exist_ok=True)
56
+ (d / "state.json").write_text(json.dumps({
57
+ "runId": run_id, "instruction": "replace the lateral joins",
58
+ "presetId": "p", "projectRoot": str(root), "status": "running",
59
+ "createdAt": "2026-08-11T09:12:47Z", "updatedAt": "2026-08-11T09:15:00Z",
60
+ "pid": self.pid,
61
+ }))
62
+
63
+ def close(self):
64
+ if self.proc:
65
+ self.proc.kill()
66
+ self.proc.wait()
67
+
68
+
69
+ class KillsLiveDeliverRun(unittest.TestCase):
70
+ def setUp(self):
71
+ self.tmp = tempfile.TemporaryDirectory()
72
+ self.root = Path(self.tmp.name)
73
+ self.fx = LiveRunFixture(self.root)
74
+
75
+ def tearDown(self):
76
+ self.fx.close()
77
+ self.tmp.cleanup()
78
+
79
+ def test_refuses_the_exact_command_from_the_journal(self):
80
+ cmd = f"kill {self.fx.pid} 2>/dev/null; touch {self.root}/.uap/deliver-runs/STOP"
81
+ self.assertEqual(esp._kills_live_deliver_run(cmd, self.root), self.fx.run_id)
82
+
83
+ def test_refuses_kill_9_and_explicit_signals(self):
84
+ for cmd in (f"kill -9 {self.fx.pid}", f"kill -TERM {self.fx.pid}", f"kill -15 {self.fx.pid}"):
85
+ self.assertTrue(esp._kills_live_deliver_run(cmd, self.root), cmd)
86
+
87
+ def test_ALLOWS_kill_0_because_that_is_a_liveness_PROBE(self):
88
+ # Refusing this would break the very check that tells a caller whether
89
+ # the run is still alive — and push it back toward killing blindly.
90
+ self.assertEqual(esp._kills_live_deliver_run(f"kill -0 {self.fx.pid}", self.root), "")
91
+
92
+ def test_ALLOWS_killing_an_unrelated_process(self):
93
+ other = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(30)"])
94
+ try:
95
+ self.assertEqual(esp._kills_live_deliver_run(f"kill {other.pid}", self.root), "")
96
+ finally:
97
+ other.kill()
98
+ other.wait()
99
+
100
+ def test_refuses_a_pattern_kill_that_would_match_the_run(self):
101
+ self.assertTrue(esp._kills_live_deliver_run("pkill -f deliver", self.root))
102
+
103
+ def test_ALLOWS_a_pattern_kill_that_matches_nothing_of_ours(self):
104
+ self.assertEqual(esp._kills_live_deliver_run("pkill -f 'http.server 8765'", self.root), "")
105
+
106
+ def test_a_recycled_pid_is_not_treated_as_a_live_run(self):
107
+ # The PID-reuse trap that once deadlocked the deliver lock: the record
108
+ # says running, but the process at that pid is something else entirely.
109
+ other = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(30)"])
110
+ try:
111
+ fx = LiveRunFixture(self.root, run_id="run-stale-aaaaaa", pid=other.pid)
112
+ self.assertEqual(esp._kills_live_deliver_run(f"kill {other.pid}", self.root), "")
113
+ finally:
114
+ other.kill()
115
+ other.wait()
116
+
117
+ def test_a_dead_pid_is_not_a_live_run(self):
118
+ dead = subprocess.Popen([sys.executable, "-c", "pass"])
119
+ dead.wait()
120
+ time.sleep(0.05)
121
+ LiveRunFixture(self.root, run_id="run-dead-bbbbbb", pid=dead.pid)
122
+ self.assertEqual(esp._kills_live_deliver_run(f"kill {dead.pid}", self.root), "")
123
+
124
+ def test_a_FINISHED_run_is_not_protected_even_if_its_process_lingers(self):
125
+ # Only a run that is still RUNNING has work to lose. One marked
126
+ # delivered or failed has finished; its process may be winding down, and
127
+ # refusing to kill that would block ordinary cleanup for no benefit.
128
+ for status in ("delivered", "failed", "interrupted"):
129
+ d = self.root / ".uap" / "deliver-runs" / self.fx.run_id
130
+ data = json.loads((d / "state.json").read_text())
131
+ data["status"] = status
132
+ (d / "state.json").write_text(json.dumps(data))
133
+ self.assertEqual(
134
+ esp._kills_live_deliver_run(f"kill {self.fx.pid}", self.root), "", status
135
+ )
136
+
137
+ def test_a_run_that_already_recorded_an_exit_is_not_live(self):
138
+ d = self.root / ".uap" / "deliver-runs" / self.fx.run_id
139
+ data = json.loads((d / "state.json").read_text())
140
+ data["exit"] = {"at": "2026-08-11T09:18:10Z", "reason": "killed by SIGTERM"}
141
+ (d / "state.json").write_text(json.dumps(data))
142
+ self.assertEqual(esp._kills_live_deliver_run(f"kill {self.fx.pid}", self.root), "")
143
+
144
+ def test_costs_nothing_when_the_command_is_not_a_kill(self):
145
+ self.assertEqual(esp._kills_live_deliver_run("uap deliver --await-run", self.root), "")
146
+ self.assertEqual(esp._kills_live_deliver_run("", self.root), "")
147
+
148
+ def test_survives_a_project_with_no_runs_at_all(self):
149
+ with tempfile.TemporaryDirectory() as empty:
150
+ self.assertEqual(esp._kills_live_deliver_run("kill 1", Path(empty)), "")
151
+
152
+ def test_survives_unreadable_run_state(self):
153
+ (self.root / ".uap" / "deliver-runs" / "run-junk-cccccc").mkdir(parents=True)
154
+ (self.root / ".uap" / "deliver-runs" / "run-junk-cccccc" / "state.json").write_text("{not json")
155
+ self.assertEqual(esp._kills_live_deliver_run(f"kill {self.fx.pid}", self.root), self.fx.run_id)
156
+
157
+
158
+ class RefusalIsActionable(unittest.TestCase):
159
+ """A refusal that does not name the alternative is how a loop survives a guard."""
160
+
161
+ def setUp(self):
162
+ self.tmp = tempfile.TemporaryDirectory()
163
+ self.root = Path(self.tmp.name)
164
+ self.fx = LiveRunFixture(self.root)
165
+
166
+ def tearDown(self):
167
+ self.fx.close()
168
+ self.tmp.cleanup()
169
+
170
+ def test_the_enforcer_refuses_end_to_end_and_names_the_cooperative_stop(self):
171
+ env = dict(os.environ)
172
+ env.pop("UAP_SELF_PROTECT_OFF", None)
173
+ env["CLAUDE_PROJECT_DIR"] = str(self.root)
174
+ enforcer = Path(__file__).resolve().parents[3] / "src" / "policies" / "enforcers" / "enforcement_self_protect.py"
175
+ r = subprocess.run(
176
+ [sys.executable, str(enforcer), "--operation", "Bash",
177
+ "--args", json.dumps({"command": f"kill {self.fx.pid} 2>/dev/null"})],
178
+ capture_output=True, text=True, cwd=str(self.root), env=env)
179
+ out = r.stdout + r.stderr
180
+ self.assertIn("RUNNING", out, out[:400])
181
+ self.assertIn("deliver-runs/STOP", out, out[:400])
182
+ self.assertIn("--await-run", out, out[:400])
183
+
184
+
185
+ if __name__ == "__main__":
186
+ unittest.main()