@miller-tech/uap 2.16.7 → 2.20.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.
Files changed (106) hide show
  1. package/config/capacity-policy.json +33 -0
  2. package/config/model-profiles/qwen38.json +32 -33
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/bin/cli.js +57 -0
  5. package/dist/bin/cli.js.map +1 -1
  6. package/dist/capacity/policy.d.ts +32 -1
  7. package/dist/capacity/policy.d.ts.map +1 -1
  8. package/dist/capacity/policy.js +123 -4
  9. package/dist/capacity/policy.js.map +1 -1
  10. package/dist/capacity/probe.d.ts +26 -0
  11. package/dist/capacity/probe.d.ts.map +1 -1
  12. package/dist/capacity/probe.js +84 -3
  13. package/dist/capacity/probe.js.map +1 -1
  14. package/dist/cli/inference.d.ts +4 -0
  15. package/dist/cli/inference.d.ts.map +1 -1
  16. package/dist/cli/inference.js +74 -8
  17. package/dist/cli/inference.js.map +1 -1
  18. package/dist/cli/merge-gate.d.ts +59 -0
  19. package/dist/cli/merge-gate.d.ts.map +1 -0
  20. package/dist/cli/merge-gate.js +238 -0
  21. package/dist/cli/merge-gate.js.map +1 -0
  22. package/dist/cli/merge-queue.d.ts +13 -0
  23. package/dist/cli/merge-queue.d.ts.map +1 -1
  24. package/dist/cli/merge-queue.js +48 -2
  25. package/dist/cli/merge-queue.js.map +1 -1
  26. package/dist/cli/mission.d.ts +25 -0
  27. package/dist/cli/mission.d.ts.map +1 -0
  28. package/dist/cli/mission.js +225 -0
  29. package/dist/cli/mission.js.map +1 -0
  30. package/dist/cli/orch.d.ts +27 -0
  31. package/dist/cli/orch.d.ts.map +1 -0
  32. package/dist/cli/orch.js +70 -0
  33. package/dist/cli/orch.js.map +1 -0
  34. package/dist/cli/systemd-services.d.ts.map +1 -1
  35. package/dist/cli/systemd-services.js +44 -0
  36. package/dist/cli/systemd-services.js.map +1 -1
  37. package/dist/delivery/merge-gate.d.ts +168 -0
  38. package/dist/delivery/merge-gate.d.ts.map +1 -0
  39. package/dist/delivery/merge-gate.js +309 -0
  40. package/dist/delivery/merge-gate.js.map +1 -0
  41. package/dist/inference/analysis.d.ts +69 -3
  42. package/dist/inference/analysis.d.ts.map +1 -1
  43. package/dist/inference/analysis.js +123 -7
  44. package/dist/inference/analysis.js.map +1 -1
  45. package/dist/inference/probe.d.ts +38 -1
  46. package/dist/inference/probe.d.ts.map +1 -1
  47. package/dist/inference/probe.js +303 -30
  48. package/dist/inference/probe.js.map +1 -1
  49. package/dist/inference/strata.d.ts +95 -0
  50. package/dist/inference/strata.d.ts.map +1 -0
  51. package/dist/inference/strata.js +167 -0
  52. package/dist/inference/strata.js.map +1 -0
  53. package/dist/mission/database.d.ts +29 -0
  54. package/dist/mission/database.d.ts.map +1 -0
  55. package/dist/mission/database.js +131 -0
  56. package/dist/mission/database.js.map +1 -0
  57. package/dist/mission/ledger.d.ts +109 -0
  58. package/dist/mission/ledger.d.ts.map +1 -0
  59. package/dist/mission/ledger.js +294 -0
  60. package/dist/mission/ledger.js.map +1 -0
  61. package/dist/mission/poll-cycle.d.ts +73 -0
  62. package/dist/mission/poll-cycle.d.ts.map +1 -0
  63. package/dist/mission/poll-cycle.js +100 -0
  64. package/dist/mission/poll-cycle.js.map +1 -0
  65. package/dist/mission/poll-executor.d.ts +91 -0
  66. package/dist/mission/poll-executor.d.ts.map +1 -0
  67. package/dist/mission/poll-executor.js +328 -0
  68. package/dist/mission/poll-executor.js.map +1 -0
  69. package/dist/models/types.d.ts.map +1 -1
  70. package/dist/models/types.js +33 -27
  71. package/dist/models/types.js.map +1 -1
  72. package/dist/self-tuning/profiles/qwen38.d.ts +13 -14
  73. package/dist/self-tuning/profiles/qwen38.d.ts.map +1 -1
  74. package/dist/self-tuning/profiles/qwen38.js +17 -18
  75. package/dist/self-tuning/profiles/qwen38.js.map +1 -1
  76. package/dist/types/mission.d.ts +80 -0
  77. package/dist/types/mission.d.ts.map +1 -0
  78. package/dist/types/mission.js +12 -0
  79. package/dist/types/mission.js.map +1 -0
  80. package/docs/architecture/adr/0003-mission-orchestrator.md +99 -0
  81. package/docs/architecture/adr/0004-deterministic-merge-gate.md +127 -0
  82. package/docs/architecture/adr/0005-ollama-compatible-surface.md +74 -0
  83. package/docs/guides/CAPACITY_POLICY.md +49 -0
  84. package/docs/guides/INFERENCE_HEALTH.md +65 -8
  85. package/docs/guides/MISSIONS.md +170 -0
  86. package/docs/guides/PROXY.md +50 -0
  87. package/docs/reports/2026-10-02-plane-admin-fail-closed.md +70 -0
  88. package/docs/reports/2026-10-02-plane-admin-protect-session.md +116 -0
  89. package/docs/visual/merge-gate.html +404 -0
  90. package/docs/visual/mission-orchestrator.html +472 -0
  91. package/package.json +2 -2
  92. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  93. package/src/policies/enforcers/__pycache__/expert_review_required.cpython-312.pyc +0 -0
  94. package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-312.pyc +0 -0
  95. package/src/policies/enforcers/plane_admin_protect.py +133 -0
  96. package/src/policies/schemas/policies/plane-admin-protect.md +55 -0
  97. package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
  98. package/templates/hooks/uap-policy-gate.sh +33 -0
  99. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  100. package/tools/agents/scripts/anthropic_proxy.py +1271 -89
  101. package/tools/agents/scripts/requirements-proxy.txt +1 -1
  102. package/tools/agents/tests/test_anthropic_proxy_streaming.py +171 -0
  103. package/tools/agents/tests/test_gate_failclosed_plane_admin.py +248 -0
  104. package/tools/agents/tests/test_ollama_compat.py +320 -0
  105. package/tools/agents/tests/test_plane_admin_protect.py +124 -0
  106. package/tools/agents/tests/test_stream_heartbeat.py +77 -0
@@ -0,0 +1,320 @@
1
+ #!/usr/bin/env python3
2
+ """Ollama-compatible surface (/api/*) on PROXY_OLLAMA_PORT.
3
+
4
+ The surface is a pure translation layer over the same guarded pipeline the
5
+ Anthropic and OpenAI inbounds use, so what needs pinning here is exactly the
6
+ translation: Ollama request fields -> OpenAI request fields, and OpenAI
7
+ response -> Ollama wire shapes (single JSON, and the two-line NDJSON stream
8
+ a stream-mode client waits on). Conversion helpers are exercised in
9
+ isolation per this suite's convention — the guarded core they call is the
10
+ same messages() pipeline the rest of the suite already covers.
11
+
12
+ The lifecycle machinery (companion listener) matters just as much: a second
13
+ uvicorn server must not install its own signal handlers (SIGTERM would land
14
+ on whichever registered last and leave the other port hanging on systemd
15
+ stops), and the app's startup handlers must not run twice.
16
+ """
17
+
18
+ import asyncio
19
+ import importlib.util
20
+ import json
21
+ import socket
22
+ import time
23
+ import unittest
24
+ from pathlib import Path
25
+
26
+
27
+ def _load():
28
+ p = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
29
+ spec = importlib.util.spec_from_file_location("anthropic_proxy", p)
30
+ m = importlib.util.module_from_spec(spec)
31
+ spec.loader.exec_module(m)
32
+ return m
33
+
34
+
35
+ proxy = _load()
36
+
37
+
38
+ class TestOllamaChatRequestConversion(unittest.TestCase):
39
+ def test_messages_options_tools_and_model_default(self):
40
+ body = {
41
+ "model": "qwen3.8-flash-next-iq3_xxs",
42
+ "messages": [
43
+ {"role": "system", "content": "be terse"},
44
+ {"role": "user", "content": "hi"},
45
+ ],
46
+ "stream": True,
47
+ "tools": [{"type": "function", "function": {"name": "f"}}],
48
+ "options": {
49
+ "temperature": 0.2,
50
+ "top_k": 40,
51
+ "num_predict": 512,
52
+ "num_ctx": 999999, # must be dropped: rail owns context sizing
53
+ "min_p": 0.05, # must be dropped: not forwarded by the 2nd hop
54
+ "stop": "END",
55
+ },
56
+ }
57
+ out = proxy.ollama_chat_to_openai_request(body)
58
+ self.assertEqual(
59
+ out["messages"],
60
+ [
61
+ {"role": "system", "content": "be terse"},
62
+ {"role": "user", "content": "hi"},
63
+ ],
64
+ )
65
+ self.assertEqual(out["model"], "qwen3.8-flash-next-iq3_xxs")
66
+ self.assertEqual(out["temperature"], 0.2)
67
+ self.assertEqual(out["top_k"], 40)
68
+ self.assertEqual(out["max_tokens"], 512)
69
+ self.assertEqual(out["stop"], ["END"])
70
+ # drops happen at the boundary, not silently mid-pipeline
71
+ for dropped in ("num_ctx", "min_p", "repeat_penalty", "seed"):
72
+ self.assertNotIn(dropped, json.dumps(out))
73
+ self.assertEqual(out["tool_choice"], "auto")
74
+ # buffered through the guardrails: always non-stream internally
75
+ self.assertFalse(out["stream"])
76
+
77
+ def test_content_parts_and_unknown_role(self):
78
+ body = {
79
+ "model": "m",
80
+ "messages": [
81
+ {"role": "supervisor", "content": [{"type": "text", "text": "part1"},
82
+ {"type": "image", "text": "drop"}]},
83
+ {"role": "user", "content": [{"type": "text", "text": "part2"}]},
84
+ ],
85
+ }
86
+ out = proxy.ollama_chat_to_openai_request(body)
87
+ self.assertEqual(out["messages"][0], {"role": "user", "content": "part1"})
88
+ self.assertEqual(out["messages"][1], {"role": "user", "content": "part2"})
89
+
90
+ def test_assistant_tool_calls_replay_object_args_to_string(self):
91
+ body = {
92
+ "model": "m",
93
+ "messages": [
94
+ {"role": "assistant", "content": "",
95
+ "tool_calls": [{"function": {"name": "bash",
96
+ "arguments": {"command": "ls"}}}]},
97
+ {"role": "tool", "content": "file_a\nfile_b"},
98
+ {"role": "user", "content": "thanks"},
99
+ ],
100
+ }
101
+ out = proxy.ollama_chat_to_openai_request(body)
102
+ entry = out["messages"][0]
103
+ self.assertEqual(entry["tool_calls"][0]["function"]["name"], "bash")
104
+ # OpenAI carries arguments as a JSON string
105
+ self.assertEqual(
106
+ json.loads(entry["tool_calls"][0]["function"]["arguments"]),
107
+ {"command": "ls"},
108
+ )
109
+ # Deterministic ids pair the replayed call with its tool result:
110
+ # Ollama associates by order, the Anthropic hop pairs by id.
111
+ call_id = entry["tool_calls"][0]["id"]
112
+ self.assertTrue(call_id)
113
+ self.assertEqual(out["messages"][1]["tool_call_id"], call_id)
114
+ self.assertEqual(out["messages"][2], {"role": "user", "content": "thanks"})
115
+
116
+ def test_format_field_is_accepted_and_dropped(self):
117
+ # response_format does not survive the second translation hop; the
118
+ # drop happens at the boundary instead of silently mid-pipeline.
119
+ out = proxy.ollama_chat_to_openai_request(
120
+ {"model": "m", "messages": [], "format": "json"}
121
+ )
122
+ self.assertNotIn("response_format", out)
123
+
124
+
125
+ class TestOllamaGenerateRequestConversion(unittest.TestCase):
126
+ def test_system_then_prompt_order(self):
127
+ out = proxy.ollama_generate_to_openai_request(
128
+ {"model": "m", "prompt": "write", "system": "you are terse"}
129
+ )
130
+ self.assertEqual(
131
+ out["messages"],
132
+ [
133
+ {"role": "system", "content": "you are terse"},
134
+ {"role": "user", "content": "write"},
135
+ ],
136
+ )
137
+
138
+ def test_prompt_only_and_missing_prompt(self):
139
+ out = proxy.ollama_generate_to_openai_request({"model": "m"})
140
+ self.assertEqual(out["messages"], [{"role": "user", "content": ""}])
141
+
142
+
143
+ class TestOllamaResponseConversion(unittest.TestCase):
144
+ OPENAI = {
145
+ "id": "x",
146
+ "model": "resolved-id",
147
+ "choices": [
148
+ {
149
+ "index": 0,
150
+ "message": {
151
+ "role": "assistant",
152
+ "content": "hello",
153
+ "tool_calls": [
154
+ {"function": {"name": "bash",
155
+ "arguments": "{\"command\": \"ls\"}"}}
156
+ ],
157
+ },
158
+ "finish_reason": "tool_calls",
159
+ }
160
+ ],
161
+ "usage": {"prompt_tokens": 11, "completion_tokens": 7},
162
+ }
163
+
164
+ def test_chat_response_shape_and_tool_args_object(self):
165
+ resp = proxy.openai_to_ollama_chat_response(self.OPENAI, "asked-id", 123)
166
+ self.assertEqual(resp["model"], "asked-id") # echo the REQUESTED id
167
+ self.assertTrue(resp["done"])
168
+ self.assertEqual(resp["done_reason"], "stop") # only "length" is special
169
+ self.assertEqual(resp["message"]["content"], "hello")
170
+ self.assertEqual(
171
+ resp["message"]["tool_calls"][0]["function"]["arguments"],
172
+ {"command": "ls"}, # Ollama carries arguments as an object
173
+ )
174
+ self.assertEqual(resp["prompt_eval_count"], 11)
175
+ self.assertEqual(resp["eval_count"], 7)
176
+ self.assertEqual(resp["total_duration"], 123)
177
+
178
+ def test_chat_response_unparseable_args_stay_string(self):
179
+ openai = {
180
+ "choices": [{"message": {"role": "assistant", "content": "",
181
+ "tool_calls": [{"function": {
182
+ "name": "f", "arguments": "{oops"}}]},
183
+ "finish_reason": "tool_calls"}],
184
+ "usage": {},
185
+ }
186
+ resp = proxy.openai_to_ollama_chat_response(openai, "m", 0)
187
+ self.assertEqual(resp["message"]["tool_calls"][0]["function"]["arguments"],
188
+ "{oops")
189
+
190
+ def test_chat_response_length_done_reason(self):
191
+ openai = {
192
+ "choices": [{"message": {"role": "assistant", "content": "x"},
193
+ "finish_reason": "length"}],
194
+ "usage": {},
195
+ }
196
+ resp = proxy.openai_to_ollama_chat_response(openai, "m", 0)
197
+ self.assertEqual(resp["done_reason"], "length")
198
+
199
+ def test_generate_response_and_tool_calls_as_text(self):
200
+ resp = proxy.openai_to_ollama_generate_response(self.OPENAI, "asked-id", 5)
201
+ self.assertTrue(resp["response"].startswith("hello"))
202
+ self.assertIn('"tool_calls"', resp["response"]) # not silently dropped
203
+ self.assertEqual(resp["eval_count"], 7)
204
+ self.assertTrue(resp["done"])
205
+
206
+ def test_ndjson_stream_two_lines_done_last(self):
207
+ resp = proxy.openai_to_ollama_chat_response(self.OPENAI, "m", 1)
208
+ out = asyncio.run(_collect(proxy._emit_ollama_ndjson(
209
+ resp, {"message": resp["message"]})))
210
+ lines = [json.loads(l) for l in out.splitlines()]
211
+ self.assertEqual(len(lines), 2)
212
+ self.assertFalse(lines[0]["done"])
213
+ self.assertEqual(lines[0]["message"]["content"], "hello")
214
+ self.assertTrue(lines[-1]["done"])
215
+ # final line carries the payload key EMPTY — concatenating content
216
+ # across lines must yield it exactly once
217
+ self.assertEqual(lines[-1]["message"], {"role": "assistant", "content": ""})
218
+ self.assertEqual(
219
+ lines[0]["message"]["content"] + lines[-1]["message"]["content"], "hello"
220
+ )
221
+ self.assertEqual(lines[-1]["eval_count"], 7)
222
+
223
+ def test_ndjson_stream_generate_empties_response_key(self):
224
+ resp = proxy.openai_to_ollama_generate_response(
225
+ {"choices": [{"message": {"role": "assistant", "content": "hi"},
226
+ "finish_reason": "stop"}], "usage": {}}, "m", 1)
227
+ out = asyncio.run(_collect(proxy._emit_ollama_ndjson(
228
+ resp, {"response": resp["response"]})))
229
+ lines = [json.loads(l) for l in out.splitlines()]
230
+ self.assertEqual(lines[0]["response"], "hi")
231
+ self.assertEqual(lines[-1]["response"], "")
232
+ self.assertTrue(lines[-1]["done"])
233
+
234
+
235
+ class TestOllamaModelCardHelpers(unittest.TestCase):
236
+ def test_quantization_and_family_from_model_id(self):
237
+ mid = "qwen3.8-flash-next-iq3_xxs"
238
+ self.assertEqual(proxy._ollama_quantization_level(mid), "IQ3_XXS")
239
+ self.assertEqual(proxy._ollama_family(mid), "qwen3")
240
+ self.assertEqual(proxy._ollama_family("llama3:8b"), "llama3")
241
+ self.assertEqual(proxy._ollama_quantization_level("plain"), "")
242
+
243
+ def test_digest_is_stable_and_prefixed_hex(self):
244
+ d1 = proxy._ollama_digest("m")
245
+ d2 = proxy._ollama_digest("m")
246
+ self.assertEqual(d1, d2)
247
+ self.assertTrue(d1.startswith("sha256:")) # some clients parse on the colon
248
+ int(d1[7:], 16) # hex
249
+ self.assertEqual(len(d1), 71)
250
+
251
+ def test_details_shape(self):
252
+ d = proxy._ollama_details("qwen3.8-flash-next-iq3_xxs")
253
+ self.assertEqual(d["format"], "gguf")
254
+ self.assertEqual(d["family"], d["families"][0])
255
+ self.assertEqual(d["quantization_level"], "IQ3_XXS")
256
+
257
+
258
+ class TestOllamaModelMeta(unittest.IsolatedAsyncioTestCase):
259
+ async def test_meta_placeholders_when_no_upstream(self):
260
+ meta = await proxy._ollama_model_meta("some-model")
261
+ self.assertEqual(meta, {"modified_at": proxy._OLLAMA_START_ISO, "size": 0})
262
+
263
+ async def test_meta_served_from_cache(self):
264
+ # These are discovery endpoints (some unauthenticated) — the meta
265
+ # probe must not hit the upstream once per request.
266
+ proxy._OLLAMA_META_CACHE.clear()
267
+ proxy._OLLAMA_META_CACHE["m"] = (
268
+ time.monotonic(), {"modified_at": "FRESH", "size": 2}
269
+ )
270
+ try:
271
+ self.assertEqual(
272
+ await proxy._ollama_model_meta("m"), {"modified_at": "FRESH", "size": 2}
273
+ )
274
+ finally:
275
+ proxy._OLLAMA_META_CACHE.clear()
276
+
277
+
278
+ class TestCompanionListenerLifecycle(unittest.TestCase):
279
+ def test_capture_signals_is_a_noop(self):
280
+ # A second uvicorn server installing signal handlers would hand
281
+ # SIGTERM to whichever registered last. The companion must not
282
+ # register any.
283
+ import signal
284
+
285
+ companion = proxy._CompanionListener.__new__(proxy._CompanionListener)
286
+ before = signal.getsignal(signal.SIGTERM)
287
+ with companion.capture_signals():
288
+ during = signal.getsignal(signal.SIGTERM)
289
+ after = signal.getsignal(signal.SIGTERM)
290
+ self.assertIs(before, during)
291
+ self.assertIs(before, after)
292
+
293
+ def test_port_bindable_detects_occupied_port(self):
294
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
295
+ s.bind(("127.0.0.1", 0))
296
+ s.listen(1)
297
+ busy_port = s.getsockname()[1]
298
+ self.assertFalse(proxy._ollama_port_bindable("127.0.0.1", busy_port))
299
+ # same port free after close
300
+ self.assertTrue(proxy._ollama_port_bindable("127.0.0.1", busy_port))
301
+
302
+ def test_port_bindable_free_port(self):
303
+ self.assertTrue(proxy._ollama_port_bindable("127.0.0.1", 0))
304
+ # port 0: bind with port 0 means "pick one" — always bindable; the
305
+ # function is only called with real ports, but must not raise.
306
+
307
+ def test_ollama_port_env_default_and_disable(self):
308
+ # module-level default (11434) was set at import
309
+ self.assertEqual(proxy.PROXY_OLLAMA_PORT, 11434)
310
+
311
+
312
+ async def _collect(agen):
313
+ parts = []
314
+ async for chunk in agen:
315
+ parts.append(chunk.decode() if isinstance(chunk, bytes) else chunk)
316
+ return "".join(parts)
317
+
318
+
319
+ if __name__ == "__main__":
320
+ unittest.main()
@@ -0,0 +1,124 @@
1
+ """Tests for plane-admin-protect: credential-mutation guard on the Plane stack.
2
+
3
+ Covers the three things that make this enforcer trustworthy:
4
+ - it catches the observed incident spelling (docker exec ... changepassword)
5
+ and the bash -c wrapper,
6
+ - it does NOT refuse commands that merely NAME a credential verb in prose
7
+ (commit message, memory store, heredoc body) — the false positives that
8
+ make an agent route around a gate,
9
+ - the owner waiver fails CLOSED (exit 2, allowed=false) on expired / missing
10
+ expiry / unreadable, rather than crashing (exit 1) or silently allowing.
11
+ """
12
+ import json
13
+ import os
14
+ import subprocess
15
+ import sys
16
+ import tempfile
17
+ import unittest
18
+ from datetime import date, timedelta
19
+ from pathlib import Path
20
+
21
+ ENF = Path(__file__).resolve().parents[3] / "src" / "policies" / "enforcers" / "plane_admin_protect.py"
22
+
23
+ MUTATION = "docker exec plane-app-api-1 python manage.py changepassword admin@pay2u.com.au"
24
+
25
+
26
+ def run(root, command, op="Bash"):
27
+ e = dict(os.environ)
28
+ e["UAP_REPO_ROOT"] = str(root)
29
+ p = subprocess.run(
30
+ [sys.executable, str(ENF), "--operation", op,
31
+ "--args", json.dumps({"command": command})],
32
+ capture_output=True, text=True, env=e,
33
+ )
34
+ out = json.loads(p.stdout) if p.stdout.strip() else {}
35
+ return p.returncode, out
36
+
37
+
38
+ def write_waiver(root, days):
39
+ w = root / "policies" / "waivers" / "plane-admin-reset"
40
+ w.parent.mkdir(parents=True, exist_ok=True)
41
+ w.write_text(f"owner approved reset\nexpires: {(date.today() + timedelta(days=days)).isoformat()}\n")
42
+ return w
43
+
44
+
45
+ class PlaneAdminProtectTest(unittest.TestCase):
46
+ def setUp(self):
47
+ self._tmp = tempfile.TemporaryDirectory()
48
+ self.root = Path(self._tmp.name)
49
+ self.addCleanup(self._tmp.cleanup)
50
+
51
+ def test_non_shell_operation_allowed(self):
52
+ rc, out = run(self.root, MUTATION, op="Edit")
53
+ self.assertEqual(rc, 0)
54
+ self.assertTrue(out["allowed"])
55
+
56
+ def test_observed_incident_blocked(self):
57
+ rc, out = run(self.root, MUTATION)
58
+ self.assertEqual(rc, 2)
59
+ self.assertFalse(out["allowed"])
60
+ self.assertIn("changepassword", out["reason"])
61
+ self.assertEqual(out.get("route"), "owner-waiver")
62
+ self.assertIn("waiverHint", out)
63
+
64
+ def test_bash_c_wrapped_mutation_blocked(self):
65
+ rc, out = run(self.root, 'bash -c "docker exec plane-app-api-1 manage.py set_password admin"')
66
+ self.assertEqual(rc, 2)
67
+ self.assertFalse(out["allowed"])
68
+
69
+ def test_read_only_plane_command_allowed(self):
70
+ rc, out = run(self.root, "kubectl logs plane-app-worker")
71
+ self.assertEqual(rc, 0)
72
+ self.assertTrue(out["allowed"])
73
+
74
+ def test_prose_naming_verb_not_blocked(self):
75
+ rc, out = run(self.root, 'git commit -m "refused to passwd /opt/plane admin"')
76
+ self.assertEqual(rc, 0)
77
+ self.assertTrue(out["allowed"])
78
+ rc, out = run(self.root, 'uap memory store "never set_password on plane-db"')
79
+ self.assertEqual(rc, 0)
80
+ self.assertTrue(out["allowed"])
81
+
82
+ def test_heredoc_body_naming_verb_not_blocked(self):
83
+ cmd = "python3 - <<PY\n# would createsuperuser on plane-db\nprint(1)\nPY"
84
+ rc, out = run(self.root, cmd)
85
+ self.assertEqual(rc, 0)
86
+ self.assertTrue(out["allowed"])
87
+
88
+ def test_valid_waiver_allows(self):
89
+ write_waiver(self.root, 3)
90
+ rc, out = run(self.root, MUTATION)
91
+ self.assertEqual(rc, 0)
92
+ self.assertTrue(out["allowed"])
93
+ self.assertIn("waiver valid until", out["reason"])
94
+
95
+ def test_expired_waiver_fails_closed(self):
96
+ write_waiver(self.root, -3)
97
+ rc, out = run(self.root, MUTATION)
98
+ self.assertEqual(rc, 2)
99
+ self.assertFalse(out["allowed"])
100
+ self.assertIn("expired", out["reason"])
101
+
102
+ def test_waiver_missing_expiry_fails_closed(self):
103
+ w = self.root / "policies" / "waivers" / "plane-admin-reset"
104
+ w.parent.mkdir(parents=True, exist_ok=True)
105
+ w.write_text("owner approved reset\n")
106
+ rc, out = run(self.root, MUTATION)
107
+ self.assertEqual(rc, 2)
108
+ self.assertFalse(out["allowed"])
109
+ self.assertIn("expires", out["reason"])
110
+
111
+ def test_unreadable_waiver_fails_closed_not_crash(self):
112
+ w = write_waiver(self.root, 3)
113
+ w.chmod(0)
114
+ try:
115
+ rc, out = run(self.root, MUTATION)
116
+ finally:
117
+ w.chmod(0o644)
118
+ self.assertEqual(rc, 2)
119
+ self.assertFalse(out["allowed"])
120
+ self.assertIn("could not be read", out["reason"])
121
+
122
+
123
+ if __name__ == "__main__":
124
+ unittest.main()
@@ -107,5 +107,82 @@ class TestStreamHeartbeat(unittest.TestCase):
107
107
  self.assertIn("kaboom", out)
108
108
 
109
109
 
110
+ class TestGuardedWaitProgressLog(unittest.TestCase):
111
+ """Journal progress during the buffered wait (2026-10-04 incident).
112
+
113
+ A no-tool streaming turn served via the guarded path can buffer for
114
+ minutes (live: an 11,099-token client-compaction summary over 281 s) with
115
+ the journal silent from REQ to RESP — the proxy read as hung and the only
116
+ evidence was the RESP timestamp. `_heartbeat_then_buffered` now logs one
117
+ INFO line per PROXY_GUARDED_WAIT_LOG_SECS of waiting so a long buffered
118
+ generation is legible while it happens.
119
+ """
120
+
121
+ def setUp(self):
122
+ self._hb = proxy.PROXY_STREAM_HEARTBEAT_SECS
123
+ self._log_secs = proxy.PROXY_GUARDED_WAIT_LOG_SECS
124
+ proxy.PROXY_STREAM_HEARTBEAT_SECS = 0.05
125
+ proxy.PROXY_GUARDED_WAIT_LOG_SECS = 0.06
126
+
127
+ def tearDown(self):
128
+ proxy.PROXY_STREAM_HEARTBEAT_SECS = self._hb
129
+ proxy.PROXY_GUARDED_WAIT_LOG_SECS = self._log_secs
130
+
131
+ @staticmethod
132
+ def _response():
133
+ return {
134
+ "id": "msg_z",
135
+ "model": "m",
136
+ "content": [{"type": "text", "text": "done"}],
137
+ "stop_reason": "end_turn",
138
+ "usage": {"output_tokens": 1},
139
+ }
140
+
141
+ def test_slow_produce_logs_wait_progress(self):
142
+ async def produce():
143
+ await asyncio.sleep(0.2) # > 2 log boundaries at 0.06s
144
+ return self._response()
145
+
146
+ with self.assertLogs("uap.anthropic_proxy", level="INFO") as cm:
147
+ out = "".join(asyncio.run(_collect(produce())))
148
+ self.assertIn("done", out) # stream still completes normally
149
+ wait_logs = [l for l in cm.output if "GUARDED-BUFFER WAIT" in l]
150
+ self.assertGreaterEqual(len(wait_logs), 2)
151
+ self.assertIn("still generating after", wait_logs[0])
152
+ self.assertIn("test-model", wait_logs[0])
153
+
154
+ def test_progress_logs_once_per_boundary_not_per_heartbeat(self):
155
+ async def produce():
156
+ await asyncio.sleep(0.22) # ~4 heartbeats, ~3 boundaries
157
+ return self._response()
158
+
159
+ with self.assertLogs("uap.anthropic_proxy", level="INFO") as cm:
160
+ asyncio.run(_collect(produce()))
161
+ wait_logs = [l for l in cm.output if "GUARDED-BUFFER WAIT" in l]
162
+ # 0.22s of waiting at 0.06s boundaries: 2-4 lines, one per boundary
163
+ # crossed — NOT one per 0.05s heartbeat.
164
+ self.assertGreaterEqual(len(wait_logs), 2)
165
+ self.assertLessEqual(len(wait_logs), 4)
166
+
167
+ def test_fast_produce_logs_no_wait_progress(self):
168
+ async def produce():
169
+ return self._response()
170
+
171
+ with self.assertNoLogs("uap.anthropic_proxy", level="INFO"):
172
+ out = "".join(asyncio.run(_collect(produce())))
173
+ self.assertIn("done", out)
174
+
175
+ def test_wait_progress_log_disabled(self):
176
+ proxy.PROXY_GUARDED_WAIT_LOG_SECS = 0.0
177
+
178
+ async def produce():
179
+ await asyncio.sleep(0.15)
180
+ return self._response()
181
+
182
+ with self.assertNoLogs("uap.anthropic_proxy", level="INFO"):
183
+ out = "".join(asyncio.run(_collect(produce())))
184
+ self.assertIn("done", out)
185
+
186
+
110
187
  if __name__ == "__main__":
111
188
  unittest.main()