@miller-tech/uap 1.196.0 → 1.196.1

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.196.0",
3
+ "version": "1.196.1",
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_schema_diff_gate 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_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_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",
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_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_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_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",
25
25
  "test:coverage": "vitest --coverage",
26
26
  "bench": "vitest --config vitest.bench.config.ts",
27
27
  "lint": "eslint src --ext .ts",
@@ -18,6 +18,7 @@ There is intentionally NO model-reachable bypass: honoring UAP_DELIVER_BYPASS he
18
18
  would reopen the hole (the model would just set it). Operator override is
19
19
  out-of-band only: UAP_SELF_PROTECT_OFF=1 in the trusted launch env.
20
20
  """
21
+ import json
21
22
  import os
22
23
  import posixpath
23
24
  import re
@@ -369,6 +370,107 @@ def _mentions_deliver_coord(command: str) -> bool:
369
370
  return any(t in text for t in _DELIVER_COORD)
370
371
 
371
372
 
373
+ # --- Signalling a LIVE deliver run -------------------------------------------
374
+ #
375
+ # The lock, the run checkpoints and the heartbeat are protected above, but the
376
+ # RUN ITSELF was not: `kill <pid>` ended a mission mid-turn and no rule saw it.
377
+ #
378
+ # Measured 2026-08-11: three consecutive runs reached turn 3, turn 8 and turn 10
379
+ # and every one was terminated from outside. The journal caught the exact shape,
380
+ # a kill of that run's own pid followed by a cooperative stop request, five
381
+ # minutes after launch. Both halves are the caller trying to stop a run; only
382
+ # the second keeps the work. The first drops the turn on the floor with the lock
383
+ # still held, and it is why "deliver never gets past planning" was believed
384
+ # while the runs were in fact working.
385
+ #
386
+ # Deliberately NARROW. Killing anything else stays allowed - a blanket refusal
387
+ # on `kill` would block ordinary process cleanup for no gain. The rule fires
388
+ # only when the target is verified to be a live deliver run.
389
+
390
+ # `kill -0` is a liveness PROBE and must stay allowed: refusing it would break
391
+ # the very check that tells a caller whether a run is still alive.
392
+ _KILL_RE = re.compile(r"\bkill\s+(?P<flag>-[\w]+\s+)?(?P<pids>[\d\s]+)")
393
+ _PKILL_RE = re.compile(r"\b(?:pkill|killall)\b(?P<rest>[^;&|]*)")
394
+ _PROBE_SIGNALS = {"-0"}
395
+
396
+
397
+ def _signalled_pids(command: str) -> set:
398
+ """PIDs a command would send a TERMINATING signal to."""
399
+ out = set()
400
+ for m in _KILL_RE.finditer(command or ""):
401
+ if (m.group("flag") or "").strip() in _PROBE_SIGNALS:
402
+ continue
403
+ for tok in (m.group("pids") or "").split():
404
+ if tok.isdigit():
405
+ out.add(int(tok))
406
+ return out
407
+
408
+
409
+ def _cmdline_of(pid: int) -> str:
410
+ try:
411
+ with open("/proc/%d/cmdline" % pid, "rb") as fh:
412
+ return fh.read().replace(b"\x00", b" ").decode("utf-8", "replace")
413
+ except OSError:
414
+ return ""
415
+
416
+
417
+ def _live_deliver_runs(root) -> dict:
418
+ """{pid: runId} for deliver runs that are RUNNING and still alive.
419
+
420
+ Verifies the pid against /proc rather than trusting state.json. Pids are
421
+ recycled, and a stale record pointing at a reused pid would otherwise refuse
422
+ an unrelated kill - the same PID-reuse trap that once deadlocked the deliver
423
+ lock. If the process is not a deliver, the record is stale and the kill is
424
+ none of this rule's business.
425
+ """
426
+ runs = {}
427
+ base = Path(root) / ".uap" / "deliver-runs"
428
+ try:
429
+ entries = list(base.iterdir())
430
+ except OSError:
431
+ return runs
432
+ for entry in entries:
433
+ try:
434
+ data = json.loads((entry / "state.json").read_text(encoding="utf-8"))
435
+ except (OSError, ValueError):
436
+ continue
437
+ pid = data.get("pid")
438
+ if not isinstance(pid, int) or data.get("status") != "running":
439
+ continue
440
+ if data.get("exit"):
441
+ continue
442
+ if "deliver" not in _cmdline_of(pid):
443
+ continue
444
+ runs[pid] = str(data.get("runId") or entry.name)
445
+ return runs
446
+
447
+
448
+ def _kills_live_deliver_run(command: str, root) -> str:
449
+ """The runId this command would terminate, or "" when it terminates none."""
450
+ if not command or not re.search(r"\b(?:kill|pkill|killall)\b", command):
451
+ return ""
452
+ live = _live_deliver_runs(root)
453
+ if not live:
454
+ return ""
455
+ for pid in _signalled_pids(command):
456
+ if pid in live:
457
+ return live[pid]
458
+ # Pattern kills name no pid: test the pattern against what the run IS.
459
+ for m in _PKILL_RE.finditer(command):
460
+ for raw in re.findall(r"""["\']([^"\']+)["\']|(\S+)""", m.group("rest") or ""):
461
+ token = (raw[0] or raw[1]).strip()
462
+ if not token or token.startswith("-"):
463
+ continue
464
+ try:
465
+ pat = re.compile(token)
466
+ except re.error:
467
+ continue
468
+ for pid, run_id in live.items():
469
+ if pat.search(_cmdline_of(pid)):
470
+ return run_id
471
+ return ""
472
+
473
+
372
474
  def _direct_destructive(command: str) -> bool:
373
475
  """Destructive op naming a protected path, judged per command segment."""
374
476
  cd_into_protected = False
@@ -532,6 +634,23 @@ def main() -> None:
532
634
  "`deliver` tool instead of disabling the gate. "
533
635
  "(Operator-only override: UAP_SELF_PROTECT_OFF=1.)",
534
636
  )
637
+ live_run = _kills_live_deliver_run(cmd, repo_root())
638
+ if live_run:
639
+ emit(
640
+ False,
641
+ "BLOCKED: that signal would kill deliver run %s, which is RUNNING "
642
+ "right now. Killing it drops the turn in flight - its work is lost "
643
+ "and the lock stays held until the next launch reclaims it. "
644
+ "Measured 2026-08-11: three runs were killed this way at turns 3, 8 "
645
+ "and 10 while they were working, and the runs were then reported as "
646
+ "'stuck in planning'. "
647
+ "To STOP it and KEEP the work, request the cooperative stop - "
648
+ "`touch .uap/deliver-runs/STOP` - which ends the run at the next "
649
+ "turn boundary with its checkpoint written and the lock released. "
650
+ "To watch it instead, `uap deliver --await-run` reports live "
651
+ "progress. (Operator-only override: UAP_SELF_PROTECT_OFF=1.)"
652
+ % live_run,
653
+ )
535
654
  if _bash_destructive(cmd):
536
655
  # Name the SURFACE that was touched. "policy enforcers or proxy env"
537
656
  # is simply untrue of a lock file, and a refusal that describes the
@@ -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()