@miller-tech/uap 2.21.3 → 2.36.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.
Files changed (328) hide show
  1. package/README.md +22 -10
  2. package/config/model-profiles/qwen38-coder390.json +46 -0
  3. package/config/model-profiles/qwen38.json +3 -2
  4. package/config/model-registry.json +110 -0
  5. package/dist/.tsbuildinfo +1 -1
  6. package/dist/benchmarks/paired/types.d.ts +2 -2
  7. package/dist/bin/cli.js +315 -20
  8. package/dist/bin/cli.js.map +1 -1
  9. package/dist/cli/attention.d.ts +6 -0
  10. package/dist/cli/attention.d.ts.map +1 -0
  11. package/dist/cli/attention.js +115 -0
  12. package/dist/cli/attention.js.map +1 -0
  13. package/dist/cli/bench-report.d.ts +82 -0
  14. package/dist/cli/bench-report.d.ts.map +1 -0
  15. package/dist/cli/bench-report.js +237 -0
  16. package/dist/cli/bench-report.js.map +1 -0
  17. package/dist/cli/clients.d.ts +6 -0
  18. package/dist/cli/clients.d.ts.map +1 -0
  19. package/dist/cli/clients.js +142 -0
  20. package/dist/cli/clients.js.map +1 -0
  21. package/dist/cli/dashboard-grants.d.ts +26 -0
  22. package/dist/cli/dashboard-grants.d.ts.map +1 -0
  23. package/dist/cli/dashboard-grants.js +69 -0
  24. package/dist/cli/dashboard-grants.js.map +1 -0
  25. package/dist/cli/dashboard-pace.d.ts +31 -0
  26. package/dist/cli/dashboard-pace.d.ts.map +1 -0
  27. package/dist/cli/dashboard-pace.js +42 -0
  28. package/dist/cli/dashboard-pace.js.map +1 -0
  29. package/dist/cli/dashboard-session.d.ts +25 -0
  30. package/dist/cli/dashboard-session.d.ts.map +1 -0
  31. package/dist/cli/dashboard-session.js +814 -0
  32. package/dist/cli/dashboard-session.js.map +1 -0
  33. package/dist/cli/dashboard-types.d.ts +18 -0
  34. package/dist/cli/dashboard-types.d.ts.map +1 -0
  35. package/dist/cli/dashboard-types.js +9 -0
  36. package/dist/cli/dashboard-types.js.map +1 -0
  37. package/dist/cli/dashboard.d.ts +2 -18
  38. package/dist/cli/dashboard.d.ts.map +1 -1
  39. package/dist/cli/dashboard.js +43 -793
  40. package/dist/cli/dashboard.js.map +1 -1
  41. package/dist/cli/deliver.d.ts +35 -0
  42. package/dist/cli/deliver.d.ts.map +1 -1
  43. package/dist/cli/deliver.js +93 -9
  44. package/dist/cli/deliver.js.map +1 -1
  45. package/dist/cli/diff.d.ts +85 -0
  46. package/dist/cli/diff.d.ts.map +1 -0
  47. package/dist/cli/diff.js +293 -0
  48. package/dist/cli/diff.js.map +1 -0
  49. package/dist/cli/eta.d.ts +35 -0
  50. package/dist/cli/eta.d.ts.map +1 -0
  51. package/dist/cli/eta.js +101 -0
  52. package/dist/cli/eta.js.map +1 -0
  53. package/dist/cli/handsfree.d.ts +7 -0
  54. package/dist/cli/handsfree.d.ts.map +1 -1
  55. package/dist/cli/handsfree.js +27 -0
  56. package/dist/cli/handsfree.js.map +1 -1
  57. package/dist/cli/hooks.d.ts.map +1 -1
  58. package/dist/cli/hooks.js +126 -5
  59. package/dist/cli/hooks.js.map +1 -1
  60. package/dist/cli/loops.d.ts +24 -5
  61. package/dist/cli/loops.d.ts.map +1 -1
  62. package/dist/cli/loops.js +92 -12
  63. package/dist/cli/loops.js.map +1 -1
  64. package/dist/cli/models.d.ts +62 -0
  65. package/dist/cli/models.d.ts.map +1 -0
  66. package/dist/cli/models.js +567 -0
  67. package/dist/cli/models.js.map +1 -0
  68. package/dist/cli/plan.d.ts +9 -0
  69. package/dist/cli/plan.d.ts.map +1 -1
  70. package/dist/cli/plan.js +57 -4
  71. package/dist/cli/plan.js.map +1 -1
  72. package/dist/cli/proxy-relay.d.ts +55 -0
  73. package/dist/cli/proxy-relay.d.ts.map +1 -0
  74. package/dist/cli/proxy-relay.js +116 -0
  75. package/dist/cli/proxy-relay.js.map +1 -0
  76. package/dist/cli/proxy.d.ts +4 -0
  77. package/dist/cli/proxy.d.ts.map +1 -1
  78. package/dist/cli/proxy.js +26 -1
  79. package/dist/cli/proxy.js.map +1 -1
  80. package/dist/cli/sandbox.d.ts +101 -4
  81. package/dist/cli/sandbox.d.ts.map +1 -1
  82. package/dist/cli/sandbox.js +393 -44
  83. package/dist/cli/sandbox.js.map +1 -1
  84. package/dist/cli/self.d.ts +13 -0
  85. package/dist/cli/self.d.ts.map +1 -0
  86. package/dist/cli/self.js +94 -0
  87. package/dist/cli/self.js.map +1 -0
  88. package/dist/cli/systemd-services.d.ts +2 -8
  89. package/dist/cli/systemd-services.d.ts.map +1 -1
  90. package/dist/cli/systemd-services.js +5 -20
  91. package/dist/cli/systemd-services.js.map +1 -1
  92. package/dist/cli/task.js +1 -1
  93. package/dist/cli/task.js.map +1 -1
  94. package/dist/cli/wizard-config.d.ts.map +1 -1
  95. package/dist/cli/wizard-config.js +31 -2
  96. package/dist/cli/wizard-config.js.map +1 -1
  97. package/dist/config/settings-registry.d.ts +1 -1
  98. package/dist/config/settings-registry.d.ts.map +1 -1
  99. package/dist/config/settings-registry.js +108 -0
  100. package/dist/config/settings-registry.js.map +1 -1
  101. package/dist/coordination/capability-router.js +2 -2
  102. package/dist/coordination/capability-router.js.map +1 -1
  103. package/dist/coordination/database.d.ts +13 -0
  104. package/dist/coordination/database.d.ts.map +1 -1
  105. package/dist/coordination/database.js +63 -7
  106. package/dist/coordination/database.js.map +1 -1
  107. package/dist/coordination/eta-inject.d.ts +32 -0
  108. package/dist/coordination/eta-inject.d.ts.map +1 -0
  109. package/dist/coordination/eta-inject.js +64 -0
  110. package/dist/coordination/eta-inject.js.map +1 -0
  111. package/dist/coordination/reactor.d.ts.map +1 -1
  112. package/dist/coordination/reactor.js +14 -0
  113. package/dist/coordination/reactor.js.map +1 -1
  114. package/dist/coordination/service.d.ts +18 -8
  115. package/dist/coordination/service.d.ts.map +1 -1
  116. package/dist/coordination/service.js +38 -28
  117. package/dist/coordination/service.js.map +1 -1
  118. package/dist/dashboard/client-registry.d.ts +163 -0
  119. package/dist/dashboard/client-registry.d.ts.map +1 -0
  120. package/dist/dashboard/client-registry.js +549 -0
  121. package/dist/dashboard/client-registry.js.map +1 -0
  122. package/dist/dashboard/controls.d.ts.map +1 -1
  123. package/dist/dashboard/controls.js +33 -4
  124. package/dist/dashboard/controls.js.map +1 -1
  125. package/dist/dashboard/data-seeder.d.ts +10 -7
  126. package/dist/dashboard/data-seeder.d.ts.map +1 -1
  127. package/dist/dashboard/data-seeder.js +31 -75
  128. package/dist/dashboard/data-seeder.js.map +1 -1
  129. package/dist/dashboard/data-service.d.ts +9 -0
  130. package/dist/dashboard/data-service.d.ts.map +1 -1
  131. package/dist/dashboard/data-service.js +32 -3
  132. package/dist/dashboard/data-service.js.map +1 -1
  133. package/dist/dashboard/placement-routes.d.ts +119 -0
  134. package/dist/dashboard/placement-routes.d.ts.map +1 -0
  135. package/dist/dashboard/placement-routes.js +331 -0
  136. package/dist/dashboard/placement-routes.js.map +1 -0
  137. package/dist/dashboard/server.d.ts.map +1 -1
  138. package/dist/dashboard/server.js +408 -2
  139. package/dist/dashboard/server.js.map +1 -1
  140. package/dist/delivery/attention.d.ts +130 -0
  141. package/dist/delivery/attention.d.ts.map +1 -0
  142. package/dist/delivery/attention.js +341 -0
  143. package/dist/delivery/attention.js.map +1 -0
  144. package/dist/delivery/decompose.d.ts +18 -0
  145. package/dist/delivery/decompose.d.ts.map +1 -1
  146. package/dist/delivery/decompose.js +22 -1
  147. package/dist/delivery/decompose.js.map +1 -1
  148. package/dist/delivery/eta-estimate.d.ts +111 -0
  149. package/dist/delivery/eta-estimate.d.ts.map +1 -0
  150. package/dist/delivery/eta-estimate.js +171 -0
  151. package/dist/delivery/eta-estimate.js.map +1 -0
  152. package/dist/delivery/eta.d.ts +23 -0
  153. package/dist/delivery/eta.d.ts.map +1 -0
  154. package/dist/delivery/eta.js +59 -0
  155. package/dist/delivery/eta.js.map +1 -0
  156. package/dist/delivery/orchestrated-mission.d.ts +10 -0
  157. package/dist/delivery/orchestrated-mission.d.ts.map +1 -1
  158. package/dist/delivery/orchestrated-mission.js +15 -1
  159. package/dist/delivery/orchestrated-mission.js.map +1 -1
  160. package/dist/delivery/plan-check.d.ts +7 -1
  161. package/dist/delivery/plan-check.d.ts.map +1 -1
  162. package/dist/delivery/plan-check.js +23 -2
  163. package/dist/delivery/plan-check.js.map +1 -1
  164. package/dist/delivery/run-state.d.ts +1 -1
  165. package/dist/delivery/run-state.d.ts.map +1 -1
  166. package/dist/delivery/run-state.js +8 -1
  167. package/dist/delivery/run-state.js.map +1 -1
  168. package/dist/delivery/task-orchestrator.d.ts +8 -3
  169. package/dist/delivery/task-orchestrator.d.ts.map +1 -1
  170. package/dist/delivery/task-orchestrator.js +75 -43
  171. package/dist/delivery/task-orchestrator.js.map +1 -1
  172. package/dist/delivery/task-sync.d.ts +21 -0
  173. package/dist/delivery/task-sync.d.ts.map +1 -1
  174. package/dist/delivery/task-sync.js +44 -8
  175. package/dist/delivery/task-sync.js.map +1 -1
  176. package/dist/delivery/task-workspace.d.ts +3 -3
  177. package/dist/delivery/task-workspace.js +3 -3
  178. package/dist/generators/self-card.d.ts +65 -0
  179. package/dist/generators/self-card.d.ts.map +1 -0
  180. package/dist/generators/self-card.js +256 -0
  181. package/dist/generators/self-card.js.map +1 -0
  182. package/dist/inference/probe.js +1 -1
  183. package/dist/inference/probe.js.map +1 -1
  184. package/dist/memory/prepopulate.d.ts.map +1 -1
  185. package/dist/memory/prepopulate.js +2 -0
  186. package/dist/memory/prepopulate.js.map +1 -1
  187. package/dist/models/analytics.d.ts +32 -0
  188. package/dist/models/analytics.d.ts.map +1 -1
  189. package/dist/models/analytics.js +68 -5
  190. package/dist/models/analytics.js.map +1 -1
  191. package/dist/models/plan-readability.d.ts +55 -0
  192. package/dist/models/plan-readability.d.ts.map +1 -0
  193. package/dist/models/plan-readability.js +339 -0
  194. package/dist/models/plan-readability.js.map +1 -0
  195. package/dist/placement/admission.d.ts +59 -0
  196. package/dist/placement/admission.d.ts.map +1 -0
  197. package/dist/placement/admission.js +197 -0
  198. package/dist/placement/admission.js.map +1 -0
  199. package/dist/placement/auto.d.ts +44 -0
  200. package/dist/placement/auto.d.ts.map +1 -0
  201. package/dist/placement/auto.js +212 -0
  202. package/dist/placement/auto.js.map +1 -0
  203. package/dist/placement/enforce.d.ts +128 -0
  204. package/dist/placement/enforce.d.ts.map +1 -0
  205. package/dist/placement/enforce.js +655 -0
  206. package/dist/placement/enforce.js.map +1 -0
  207. package/dist/placement/idle.d.ts +50 -0
  208. package/dist/placement/idle.d.ts.map +1 -0
  209. package/dist/placement/idle.js +213 -0
  210. package/dist/placement/idle.js.map +1 -0
  211. package/dist/placement/ledger.d.ts +323 -0
  212. package/dist/placement/ledger.d.ts.map +1 -0
  213. package/dist/placement/ledger.js +263 -0
  214. package/dist/placement/ledger.js.map +1 -0
  215. package/dist/placement/preview.d.ts +51 -0
  216. package/dist/placement/preview.d.ts.map +1 -0
  217. package/dist/placement/preview.js +61 -0
  218. package/dist/placement/preview.js.map +1 -0
  219. package/dist/placement/probes.d.ts +47 -0
  220. package/dist/placement/probes.d.ts.map +1 -0
  221. package/dist/placement/probes.js +207 -0
  222. package/dist/placement/probes.js.map +1 -0
  223. package/dist/placement/proxy-env.d.ts +21 -0
  224. package/dist/placement/proxy-env.d.ts.map +1 -0
  225. package/dist/placement/proxy-env.js +42 -0
  226. package/dist/placement/proxy-env.js.map +1 -0
  227. package/dist/placement/registry.d.ts +1038 -0
  228. package/dist/placement/registry.d.ts.map +1 -0
  229. package/dist/placement/registry.js +274 -0
  230. package/dist/placement/registry.js.map +1 -0
  231. package/dist/policies/liveness.d.ts +6 -8
  232. package/dist/policies/liveness.d.ts.map +1 -1
  233. package/dist/policies/liveness.js +6 -25
  234. package/dist/policies/liveness.js.map +1 -1
  235. package/dist/policies/sandbox-anchor.d.ts +91 -0
  236. package/dist/policies/sandbox-anchor.d.ts.map +1 -0
  237. package/dist/policies/sandbox-anchor.js +187 -0
  238. package/dist/policies/sandbox-anchor.js.map +1 -0
  239. package/dist/policies/sandbox-grants.d.ts +131 -0
  240. package/dist/policies/sandbox-grants.d.ts.map +1 -0
  241. package/dist/policies/sandbox-grants.js +562 -0
  242. package/dist/policies/sandbox-grants.js.map +1 -0
  243. package/dist/state/manifest.d.ts +4 -2
  244. package/dist/state/manifest.d.ts.map +1 -1
  245. package/dist/state/manifest.js +32 -16
  246. package/dist/state/manifest.js.map +1 -1
  247. package/dist/utils/checkout-root.d.ts +2 -0
  248. package/dist/utils/checkout-root.d.ts.map +1 -0
  249. package/dist/utils/checkout-root.js +43 -0
  250. package/dist/utils/checkout-root.js.map +1 -0
  251. package/dist/utils/resolve-uap-bin.d.ts +5 -0
  252. package/dist/utils/resolve-uap-bin.d.ts.map +1 -0
  253. package/dist/utils/resolve-uap-bin.js +33 -0
  254. package/dist/utils/resolve-uap-bin.js.map +1 -0
  255. package/dist/utils/terminal-safe.d.ts +12 -0
  256. package/dist/utils/terminal-safe.d.ts.map +1 -0
  257. package/dist/utils/terminal-safe.js +53 -0
  258. package/dist/utils/terminal-safe.js.map +1 -0
  259. package/docs/ATTRIBUTION.md +1 -0
  260. package/docs/DASHBOARD_UPLIFT_SPEC.md +8 -8
  261. package/docs/INDEX.md +6 -5
  262. package/docs/architecture/OVERVIEW.md +15 -3
  263. package/docs/architecture/PROTOCOL.md +7 -1
  264. package/docs/architecture/adr/0006-operator-directed-model-placement.md +86 -0
  265. package/docs/architecture/adr/0007-auto-displacement-allowlist-as-standing-consent.md +79 -0
  266. package/docs/architecture/adr/0008-fleet-clients-link-out.md +92 -0
  267. package/docs/architecture/adr/0009-completion-triggered-dispatch-and-per-leaf-model-dials.md +71 -0
  268. package/docs/architecture/adr/0010-attention-queue-and-afk-auto-decider.md +159 -0
  269. package/docs/architecture/adr/0011-sandbox-grants-and-network-egress-boundary.md +219 -0
  270. package/docs/architecture/adr/0012-operator-legibility-surfaces.md +175 -0
  271. package/docs/getting-started/INSTALLATION.md +1 -1
  272. package/docs/guides/AUTOMATIC_FEATURES.md +8 -0
  273. package/docs/guides/LOCAL_MODELS.md +1 -1
  274. package/docs/guides/PROXY.md +2 -1
  275. package/docs/guides/SANDBOX.md +91 -4
  276. package/docs/integrations/MCP_ROUTER.md +1 -1
  277. package/docs/plans/model-performance-proof-options.md +176 -0
  278. package/docs/reference/API.md +1 -1
  279. package/docs/reference/CLI.md +169 -4
  280. package/docs/reference/COMPARISON.md +1 -1
  281. package/docs/reference/CONFIGURATION.md +12 -3
  282. package/docs/reference/CONFIGURATION_REFERENCE.md +269 -14
  283. package/docs/reference/DATABASE_SCHEMA.md +1 -1
  284. package/docs/reference/FEATURES.md +15 -3
  285. package/docs/reference/PATTERNS.md +1 -1
  286. package/docs/reference/PLATFORMS.md +14 -1
  287. package/docs/specs/operator-model-placement.md +1314 -0
  288. package/docs/specs/operator-model-placement.review.md +339 -0
  289. package/package.json +2 -2
  290. package/scripts/maintenance/publish-npm.sh +23 -11
  291. package/scripts/version-bump.sh +6 -51
  292. package/src/policies/enforcers/__pycache__/_common.cpython-311.pyc +0 -0
  293. package/src/policies/enforcers/__pycache__/expert_review_required.cpython-311.pyc +0 -0
  294. package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-311.pyc +0 -0
  295. package/src/policies/schemas/policies/mandatory-testing-deployment.md +2 -5
  296. package/templates/hooks/__pycache__/deliver_autoroute.cpython-311.pyc +0 -0
  297. package/templates/hooks/session-start.sh +44 -0
  298. package/tools/agents/UAP/cli.py +14 -3
  299. package/tools/agents/UAP/paths.py +50 -0
  300. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-311.pyc +0 -0
  301. package/tools/agents/scripts/anthropic_proxy.py +1386 -99
  302. package/tools/agents/scripts/toolcall_path_normalizer.py +46 -1
  303. package/tools/agents/tests/test_anthropic_proxy_streaming.py +29 -6
  304. package/tools/agents/tests/test_doubling_break.py +64 -5
  305. package/tools/agents/tests/test_error_loop_break.py +10 -4
  306. package/tools/agents/tests/test_error_loop_ignores_correctives.py +13 -2
  307. package/tools/agents/tests/test_lockstep_escalation.py +280 -9
  308. package/tools/agents/tests/test_path_containment.py +94 -0
  309. package/tools/agents/tests/test_proxy_unix_socket.py +162 -0
  310. package/tools/agents/tests/test_signature_quality.py +349 -0
  311. package/tools/agents/tests/test_uap_compliance.py +11 -1
  312. package/web/dash/core.js +1 -0
  313. package/web/dash/tab-clients.js +196 -0
  314. package/web/dash/tab-models.js +387 -8
  315. package/web/dash/tab-policies.js +87 -4
  316. package/web/dash/tabs.js +77 -73
  317. package/web/dashboard.html +6 -0
  318. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  319. package/src/policies/enforcers/__pycache__/expert_review_required.cpython-312.pyc +0 -0
  320. package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-312.pyc +0 -0
  321. package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
  322. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  323. package/web/dash/tab-agents.js +0 -21
  324. package/web/dash/tab-deliver.js +0 -12
  325. package/web/dash/tab-memory.js +0 -12
  326. package/web/dash/tab-orchestration.js +0 -21
  327. package/web/dash/tab-overview.js +0 -21
  328. package/web/dash/tab-tasks.js +0 -21
@@ -144,6 +144,100 @@ class TestContainToolUses(unittest.TestCase):
144
144
  tus = [_tu("t1", file_path="/home/cogtec/dev/octus_invaders/a.js")]
145
145
  self.assertEqual(N.contain_tool_uses(tus, ""), [])
146
146
 
147
+ def _wd(self, *parts):
148
+ # Journal geometry: the 2026-10-18 incident workdir .../p37/e2e made
149
+ # the 'e2e' basename ANCHOR the container path /tmp/e2e/..., which is
150
+ # what let containment misclassify it as a garbled host path. Tests
151
+ # must reproduce that anchor or they do not discriminate.
152
+ wd = os.path.join(self._tmp.name, *parts)
153
+ os.makedirs(wd, exist_ok=True)
154
+ return wd
155
+
156
+ def test_docker_exec_container_paths_left_alone(self):
157
+ # Regression 2026-10-18, journal geometry exactly: the session
158
+ # workdir .../p37/e2e anchored the container path /tmp/e2e/... and
159
+ # pre-fix code snapped it onto the workdir -- an 11-hour
160
+ # deterministic-failure loop. The fix passes the command verbatim.
161
+ wd = self._wd("p37", "e2e")
162
+ cmd = ("docker exec p37koral bash -lc "
163
+ "'cat /tmp/e2e/koral-p59probe_proda4.log'")
164
+ tus = [{"type": "tool_use", "id": "t1", "name": "Bash",
165
+ "input": {"command": cmd}}]
166
+ self.assertEqual(N.contain_tool_uses(tus, wd), [])
167
+ self.assertEqual(tus[0]["input"]["command"], cmd)
168
+
169
+ def test_container_exec_variants_left_alone(self):
170
+ # Every variant uses the anchoring geometry so each discriminates:
171
+ # pre-fix, each gets snapped (1 correction); post-fix, all pass
172
+ # verbatim. DOCKER pins the case-insensitive match.
173
+ wd = self._wd("p37", "e2e")
174
+ for cmd in (
175
+ "kubectl -n prod exec koral-0 -- cat /tmp/e2e/app.log",
176
+ "docker compose exec koral cat /tmp/e2e/config.yaml",
177
+ "docker-compose exec koral cat /tmp/e2e/config.yaml",
178
+ "sudo podman exec ctr1 ls /tmp/e2e/data",
179
+ "DOCKER exec ctr1 cat /tmp/e2e/UP.LOG",
180
+ ):
181
+ tus = [{"type": "tool_use", "id": "t1", "name": "Bash",
182
+ "input": {"command": cmd}}]
183
+ self.assertEqual(N.contain_tool_uses(tus, wd), [], msg=cmd)
184
+ self.assertEqual(tus[0]["input"]["command"], cmd)
185
+
186
+ def test_host_path_before_docker_exec_still_contained(self):
187
+ # Boundary is the END of the exec word: the garbled HOST path
188
+ # before the subcommand still snaps; the container path after it
189
+ # passes verbatim. Full-command equality so any drift fails.
190
+ wd = self._wd("e2e")
191
+ cmd = ("mkdir -p /home/cogtk/dev/e2e/css && "
192
+ "docker exec koral cat /tmp/e2e/x.log")
193
+ tus = [{"type": "tool_use", "id": "t1", "name": "Bash",
194
+ "input": {"command": cmd}}]
195
+ corr = N.contain_tool_uses(tus, wd)
196
+ self.assertEqual(tus[0]["input"]["command"],
197
+ f"mkdir -p {wd}/css && docker exec koral cat /tmp/e2e/x.log")
198
+ self.assertEqual(len(corr), 1)
199
+
200
+ def test_engine_flag_path_before_exec_still_contained(self):
201
+ # Pins the END-of-exec-word boundary: a host-side engine flag
202
+ # (kubectl --kubeconfig=...) sits before the subcommand and stays
203
+ # contained; the container payload after it is exempt.
204
+ wd = self._wd("e2e")
205
+ cmd = ("kubectl --kubeconfig=/home/cogtk/dev/e2e/kc.conf exec koral-0 "
206
+ "-- cat /tmp/e2e/x.log")
207
+ tus = [{"type": "tool_use", "id": "t1", "name": "Bash",
208
+ "input": {"command": cmd}}]
209
+ corr = N.contain_tool_uses(tus, wd)
210
+ self.assertEqual(tus[0]["input"]["command"],
211
+ f"kubectl --kubeconfig={wd}/kc.conf exec koral-0 "
212
+ "-- cat /tmp/e2e/x.log")
213
+ self.assertEqual(len(corr), 1)
214
+
215
+ def test_exec_exempt_scope_is_sticky_to_end_of_command(self):
216
+ # Pinned semantics (architect + security reviews): once a
217
+ # container-exec boundary is found, the exemption covers the whole
218
+ # remainder, INCLUDING later host-side segments after &&. Narrowing
219
+ # to the next separator would need quote-awareness a regex cannot
220
+ # provide (bash -lc 'cat /a; cat /b') and would reintroduce the
221
+ # incident class. Fail-safe: an exempted garble errors loudly.
222
+ wd = self._wd("e2e")
223
+ cmd = ("docker exec koral cat /tmp/e2e/x.log "
224
+ "&& cat /home/cogtk/dev/e2e/creds.txt")
225
+ tus = [{"type": "tool_use", "id": "t1", "name": "Bash",
226
+ "input": {"command": cmd}}]
227
+ self.assertEqual(N.contain_tool_uses(tus, wd), [])
228
+ self.assertEqual(tus[0]["input"]["command"], cmd)
229
+
230
+ def test_exec_decoy_still_triggers_exemption(self):
231
+ # A quoted "docker exec" decoy triggers the exemption too -- accepted
232
+ # fail-safe semantics: the worst case is an un-healed garble failing
233
+ # loudly (pre-normalizer behavior), never a wrong-write.
234
+ wd = self._wd("e2e")
235
+ cmd = 'echo "docker exec" && cat /tmp/e2e/creds.txt'
236
+ tus = [{"type": "tool_use", "id": "t1", "name": "Bash",
237
+ "input": {"command": cmd}}]
238
+ self.assertEqual(N.contain_tool_uses(tus, wd), [])
239
+ self.assertEqual(tus[0]["input"]["command"], cmd)
240
+
147
241
 
148
242
  if __name__ == "__main__":
149
243
  unittest.main()
@@ -0,0 +1,162 @@
1
+ """Tests for the PROXY_SOCKET unix-socket surface (bridge P4: D1 model-path
2
+ choke point).
3
+
4
+ The socket is a permission-gated transport for the model gateway — only the
5
+ owning user can dial it (0600 socket in a 0700 parent) — serving alongside
6
+ the TCP port. Its preparation is the boundary itself: correct bind, tight
7
+ permissions, stale-corpse cleanup, refusal to evict a live proxy's socket
8
+ (including one that cannot be dialed — EACCES is not proof of death), refusal
9
+ to replace a non-socket file at the path, and a warning when the parent dir
10
+ is group/other-writable.
11
+ """
12
+ import contextlib
13
+ import importlib.util
14
+ import os
15
+ import shutil
16
+ import socket
17
+ import stat
18
+ import tempfile
19
+ import unittest
20
+ from pathlib import Path
21
+
22
+
23
+ def _load_proxy_module():
24
+ proxy_path = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
25
+ spec = importlib.util.spec_from_file_location("anthropic_proxy", proxy_path)
26
+ module = importlib.util.module_from_spec(spec)
27
+ spec.loader.exec_module(module)
28
+ return module
29
+
30
+
31
+ proxy = _load_proxy_module()
32
+
33
+
34
+ class ProxySocketPrepTest(unittest.TestCase):
35
+ def setUp(self):
36
+ self.tmp = tempfile.mkdtemp(prefix="uap-sock-")
37
+ # The parent dir exists up front so tests can bind corpses directly.
38
+ os.makedirs(os.path.join(self.tmp, "sub"))
39
+ self.sock_path = os.path.join(self.tmp, "sub", "proxy.sock")
40
+
41
+ def tearDown(self):
42
+ shutil.rmtree(self.tmp, ignore_errors=True)
43
+
44
+ def _close_quietly(self, sock):
45
+ # Teardown: an already-closed socket (uvicorn may have shut it down)
46
+ # must not fail the test run. contextlib.suppress states that intent.
47
+ with contextlib.suppress(OSError):
48
+ sock.close()
49
+
50
+ def test_binds_with_tight_permissions(self):
51
+ prepared = proxy._prepare_proxy_socket(self.sock_path)
52
+ self.assertIsNotNone(prepared)
53
+ try:
54
+ mode = stat.S_IMODE(os.stat(self.sock_path).st_mode)
55
+ self.assertEqual(mode, 0o600, "the socket is the egress boundary; 0600 or nothing")
56
+ self.assertTrue(proxy._socket_is_live(self.sock_path))
57
+ finally:
58
+ self._close_quietly(prepared)
59
+
60
+ def test_creates_a_missing_parent_dir_with_0700(self):
61
+ # A deep path whose leaf directory does not exist yet: the surface
62
+ # must create it PRIVATE (0700) — a world-readable parent would let
63
+ # any local user name-squat the socket path.
64
+ deep = os.path.join(self.tmp, "a", "b", "c", "proxy.sock")
65
+ prepared = proxy._prepare_proxy_socket(deep)
66
+ self.assertIsNotNone(prepared)
67
+ try:
68
+ leaf = os.path.dirname(deep)
69
+ mode = stat.S_IMODE(os.stat(leaf).st_mode)
70
+ self.assertEqual(mode & 0o777, 0o700, "parent dirs of the socket must be 0700")
71
+ self.assertTrue(proxy._socket_is_live(deep))
72
+ finally:
73
+ self._close_quietly(prepared)
74
+
75
+ def test_binds_when_the_parent_exists_with_loose_perms(self):
76
+ # The pre-existing-parent case must not regress: bind still works
77
+ # and the SOCKET itself stays 0600 (the parent is left as the
78
+ # operator made it — chmod-ing a dir the operator owns would be a
79
+ # surprising side effect).
80
+ os.chmod(os.path.dirname(self.sock_path), 0o755)
81
+ prepared = proxy._prepare_proxy_socket(self.sock_path)
82
+ self.assertIsNotNone(prepared)
83
+ try:
84
+ mode = stat.S_IMODE(os.stat(self.sock_path).st_mode)
85
+ self.assertEqual(mode, 0o600)
86
+ finally:
87
+ self._close_quietly(prepared)
88
+
89
+ def test_unlinks_a_stale_corpse_and_rebinds(self):
90
+ # A corpse from a crashed run: nothing accepts on the other end.
91
+ corpse = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
92
+ corpse.bind(self.sock_path)
93
+ corpse.close() # bound but never listening => connect() refused
94
+ prepared = proxy._prepare_proxy_socket(self.sock_path)
95
+ self.assertIsNotNone(prepared, "a stale socket must be reclaimed, not fatal")
96
+ self.assertTrue(proxy._socket_is_live(self.sock_path))
97
+ self._close_quietly(prepared)
98
+
99
+ def test_refuses_to_evict_a_live_proxy(self):
100
+ live = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
101
+ live.bind(self.sock_path)
102
+ live.listen(8)
103
+ try:
104
+ prepared = proxy._prepare_proxy_socket(self.sock_path)
105
+ self.assertIsNone(prepared, "a LIVE socket must be left to its owner")
106
+ self.assertTrue(proxy._socket_is_live(self.sock_path))
107
+ finally:
108
+ live.close()
109
+
110
+ def test_refuses_to_evict_a_live_socket_it_cannot_dial(self):
111
+ # EACCES must read as LIVE, not corpse (P4 security review): another
112
+ # user's 0600 socket in a shared dir cannot be proven dead, and
113
+ # evicting it would let this process impersonate their endpoint.
114
+ # Simulated same-user: chmod 000 on the socket makes connect() fail
115
+ # with EACCES while the server keeps accepting on its own fd.
116
+ live = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
117
+ live.bind(self.sock_path)
118
+ live.listen(8)
119
+ os.chmod(self.sock_path, 0o000)
120
+ try:
121
+ prepared = proxy._prepare_proxy_socket(self.sock_path)
122
+ self.assertIsNone(prepared, "an undialable LIVE socket must not be evicted")
123
+ self.assertTrue(os.path.exists(self.sock_path), "the socket file must survive")
124
+ finally:
125
+ os.chmod(self.sock_path, 0o600)
126
+ live.close()
127
+
128
+ def test_never_replaces_a_regular_file(self):
129
+ # PROXY_SOCKET can arrive from the operator's env or .uap/proxy.env
130
+ # (inside the sandbox-writable checkout): a typo or planted value
131
+ # must not become an arbitrary-file-delete primitive. The file must
132
+ # survive byte-for-byte.
133
+ data = b'{"important": "operator data"}'
134
+ with open(self.sock_path, "wb") as f:
135
+ f.write(data)
136
+ prepared = proxy._prepare_proxy_socket(self.sock_path)
137
+ self.assertIsNone(prepared, "a non-socket at the path must disable the surface")
138
+ with open(self.sock_path, "rb") as f:
139
+ self.assertEqual(f.read(), data, "the file must be untouched")
140
+
141
+ def test_warns_when_the_parent_dir_is_shared(self):
142
+ # A group/other-writable parent lets another local user unlink or
143
+ # squat the socket path; the operator gets the consequence named.
144
+ os.chmod(os.path.dirname(self.sock_path), 0o770)
145
+ try:
146
+ with self.assertLogs("uap.anthropic_proxy", level="WARNING") as logs:
147
+ prepared = proxy._prepare_proxy_socket(self.sock_path)
148
+ self.assertIsNotNone(prepared)
149
+ self.assertTrue(any("group/other-writable" in line for line in logs.output), logs.output)
150
+ self._close_quietly(prepared)
151
+ finally:
152
+ os.chmod(os.path.dirname(self.sock_path), 0o755)
153
+
154
+ def test_empty_and_too_long_paths_disable_the_surface(self):
155
+ self.assertIsNone(proxy._prepare_proxy_socket(""))
156
+ too_long = os.path.join(self.tmp, "x" * 130 + ".sock")
157
+ self.assertIsNone(proxy._prepare_proxy_socket(too_long))
158
+ self.assertFalse(os.path.exists(too_long))
159
+
160
+
161
+ if __name__ == "__main__":
162
+ unittest.main()
@@ -0,0 +1,349 @@
1
+ #!/usr/bin/env python3
2
+ """Loop-steer signature quality: the ledger classes the guards mis-sighted.
3
+
4
+ Built from the loop-incident ledger after the v2.33/v2.34 uplift on this branch:
5
+ - 2026-09-25: three distinct bash failures shared the content-free
6
+ "Traceback (most recent call last):" signature and escalated to a hard
7
+ stop that never named the real error.
8
+ - 2026-10-08: a worktree status listing whose slug contains "error"
9
+ ("openobserve-error-scan") became the signature of a healthy progress
10
+ report.
11
+ - 2026-10-17: reading a shell script containing a die() helper entered the
12
+ failure streak -- a client that never sends is_error made file CONTENT
13
+ look like a tool failure.
14
+ - 2026-10-18: different execute failures shared the content-free
15
+ "error: command failed (exit code: #)" wrapper and manufactured a
16
+ lockstep streak of 4 across distinct calls.
17
+
18
+ The ladder these tests pin: a traceback anchors on its decisive line; a real
19
+ error line anchors the signature; a wrapper-only result anchors on its first
20
+ meaningful line; and a failure that cannot be named yields "" -- DOUBLING-DOWN
21
+ owns that shape, not a signature streak.
22
+ """
23
+ import importlib.util
24
+ import unittest
25
+ from pathlib import Path
26
+
27
+
28
+ def _load_proxy_module():
29
+ proxy_path = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
30
+ spec = importlib.util.spec_from_file_location("anthropic_proxy", proxy_path)
31
+ mod = importlib.util.module_from_spec(spec)
32
+ spec.loader.exec_module(mod)
33
+ return mod
34
+
35
+
36
+ proxy = _load_proxy_module()
37
+
38
+ TRACEBACK_FULL = (
39
+ "Traceback (most recent call last):\n"
40
+ ' File "/app/run.py", line 3, in <module>\n'
41
+ " raise ValueError('boom')\n"
42
+ "ValueError: boom"
43
+ )
44
+ WRAPPER_ONLY = "error: command failed (exit code: 1)"
45
+ WRAPPER_WITH_STDERR = (
46
+ "error: command failed (exit code: 1)\n"
47
+ 'psql: error: connection to server at "plane-db" (10.5.0.2), port 5432 '
48
+ "failed: fe_sendauth: no password supplied"
49
+ )
50
+ WRAPPER_WITH_OUTPUT = "error: command failed (exit code: 1)\nCompiling main.c"
51
+ WORKTREE_STATUS = (
52
+ "wt-3 in_progress | worktree: openobserve-error-scan "
53
+ "(feature/263-stuck-break-hard)"
54
+ )
55
+ SHELL_SOURCE_READ = "die() { printf 'fatal: %s\\n' \"$*\" >&2; exit 1; }\n"
56
+
57
+
58
+ class TracebackAnchoringTests(unittest.TestCase):
59
+ def test_decisive_line_anchors_not_the_header(self):
60
+ sig = proxy._error_signature(TRACEBACK_FULL)
61
+ self.assertEqual(sig, "valueerror: boom")
62
+ self.assertNotIn("traceback", sig)
63
+
64
+ def test_decisive_line_found_when_not_keyword_shaped(self):
65
+ # SystemExit is a real decisive line but carries no _ERROR_LINE_RE
66
+ # token, so only traceback anchoring can find it.
67
+ text = (
68
+ "Traceback (most recent call last):\n"
69
+ ' File "/app/x.py", line 7, in <module>\n'
70
+ "SystemExit: 1"
71
+ )
72
+ self.assertEqual(proxy._error_signature(text), "systemexit: #")
73
+
74
+ def test_truncated_traceback_anchors_on_last_visible_frame(self):
75
+ # Token-limit cut-off before the exception line: the frames still
76
+ # identify the failing call, which is all a streak needs. (The path
77
+ # normalizer swallowing the trailing quote+comma is pre-existing and
78
+ # stable, so the signature remains edit-invariant.)
79
+ text = (
80
+ "Traceback (most recent call last):\n"
81
+ ' File "/app/x.py", line 3, in main\n'
82
+ )
83
+ self.assertEqual(proxy._error_signature(text), 'file "<path> line #, in main')
84
+
85
+ def test_header_alone_names_nothing(self):
86
+ self.assertEqual(
87
+ proxy._error_signature("Traceback (most recent call last):"), ""
88
+ )
89
+
90
+ def test_mid_payload_traceback_still_anchors(self):
91
+ # code-quality review 2026-10-18: without re.MULTILINE on the header
92
+ # regex, ^ never matched after leading output and the decisive ladder
93
+ # was skipped for every mid-payload traceback.
94
+ text = (
95
+ "building frontend...\n"
96
+ "ok\n"
97
+ "Traceback (most recent call last):\n"
98
+ ' File "/app/run.py", line 3, in <module>\n'
99
+ " raise KeyError('x')\n"
100
+ "KeyError: 'x'"
101
+ )
102
+ self.assertEqual(proxy._error_signature(text), "keyerror: 'x'")
103
+
104
+ def test_mid_payload_truncated_traceback_anchors_on_last_frame(self):
105
+ text = (
106
+ "starting\n"
107
+ "Traceback (most recent call last):\n"
108
+ ' File "/app/x.py", line 3, in main\n'
109
+ )
110
+ self.assertEqual(
111
+ proxy._error_signature(text), 'file "<path> line #, in main'
112
+ )
113
+
114
+ def test_chained_traceback_names_the_final_exception(self):
115
+ # Each traceback opens with its own header, which resets the decisive
116
+ # line, so the LAST traceback's exception is the signature.
117
+ text = (
118
+ "Traceback (most recent call last):\n"
119
+ ' File "/app/run.py", line 3, in <module>\n'
120
+ " raise KeyError('x')\n"
121
+ "KeyError: 'x'\n"
122
+ "\n"
123
+ "During handling of the above exception, another exception "
124
+ "occurred:\n"
125
+ "\n"
126
+ "Traceback (most recent call last):\n"
127
+ ' File "/app/handle.py", line 9, in recover\n'
128
+ "TypeError: cannot convert\n"
129
+ )
130
+ self.assertEqual(
131
+ proxy._error_signature(text), "typeerror: cannot convert"
132
+ )
133
+
134
+ def test_trailing_wrapper_exception_cannot_hijack_the_signature(self):
135
+ # code-quality review 2026-10-18: the last-decisive-wins overwrite let
136
+ # a trailing "...RetryError:" rename a KeyError failure.
137
+ text = TRACEBACK_FULL + "\nrequests.exceptions.RetryError: retrying"
138
+ self.assertEqual(proxy._error_signature(text), "valueerror: boom")
139
+
140
+
141
+ class ContentFreeWrapperTests(unittest.TestCase):
142
+ def test_wrapper_alone_names_nothing(self):
143
+ self.assertEqual(proxy._error_signature(WRAPPER_ONLY), "")
144
+ self.assertEqual(proxy._error_signature("Exit code 1"), "")
145
+
146
+ def test_wrapper_with_stderr_anchors_on_stderr(self):
147
+ sig = proxy._error_signature(WRAPPER_WITH_STDERR)
148
+ self.assertTrue(sig.startswith("psql: "))
149
+ self.assertIn("fe_sendauth", sig)
150
+
151
+ def test_wrapper_with_only_output_anchors_on_first_line(self):
152
+ self.assertEqual(
153
+ proxy._error_signature(WRAPPER_WITH_OUTPUT), "compiling main.c"
154
+ )
155
+
156
+ def test_unrelated_wrapper_failures_stop_sharing_a_signature(self):
157
+ # The 2026-10-18 incident: distinct failures manufactured one streak.
158
+ a = proxy._error_signature(WRAPPER_WITH_OUTPUT)
159
+ b = proxy._error_signature(
160
+ "error: command failed (exit code: 1)\nLinking libfoo.so"
161
+ )
162
+ self.assertNotEqual(a, b)
163
+ # ...while the identical failing call keeps a stable one, so LOCKSTEP
164
+ # stays armed for real lockstep loops.
165
+ self.assertEqual(
166
+ proxy._error_signature(WRAPPER_WITH_OUTPUT),
167
+ proxy._error_signature(WRAPPER_WITH_OUTPUT),
168
+ )
169
+
170
+
171
+ class EmbeddedTokenTests(unittest.TestCase):
172
+ def test_status_listing_with_error_slug_is_not_a_failure(self):
173
+ # The exact shape of the 2026-10-08 incident.
174
+ self.assertEqual(proxy._error_signature(WORKTREE_STATUS), "")
175
+
176
+ def test_real_error_lines_survive_the_embedded_filter(self):
177
+ combined = (
178
+ WORKTREE_STATUS
179
+ + "\npsql: error: connection to server failed: fe_sendauth: "
180
+ "no password supplied"
181
+ )
182
+ self.assertTrue(proxy._error_signature(combined).startswith("psql: "))
183
+ self.assertNotEqual(
184
+ proxy._error_signature("FATAL: password authentication failed"), ""
185
+ )
186
+ self.assertNotEqual(
187
+ proxy._error_signature("error: relation does not exist"), ""
188
+ )
189
+
190
+
191
+ class ReadOnlyContentTests(unittest.TestCase):
192
+ """A read whose client never declared is_error is file CONTENT (ledger
193
+ 2026-10-17: a shell script's die() helper made a healthy Read fail)."""
194
+
195
+ def test_read_source_without_flag_forms_no_signature(self):
196
+ m = proxy.SessionMonitor()
197
+ for _ in range(4):
198
+ m.note_tool_result_error(SHELL_SOURCE_READ, None, tool_names=["read"])
199
+ self.assertEqual(m.error_signature_streak, 0)
200
+ self.assertEqual(m.last_error_signature, "")
201
+
202
+ def test_declared_error_on_read_still_counts(self):
203
+ m = proxy.SessionMonitor()
204
+ m.note_tool_result_error(SHELL_SOURCE_READ, True, tool_names=["read"])
205
+ self.assertEqual(m.error_signature_streak, 1)
206
+
207
+ def test_bash_without_flag_keeps_heuristics(self):
208
+ m = proxy.SessionMonitor()
209
+ m.note_tool_result_error(SHELL_SOURCE_READ, None, tool_names=["bash"])
210
+ self.assertEqual(m.error_signature_streak, 1)
211
+
212
+ def test_unknown_tool_name_keeps_heuristics(self):
213
+ # A result whose producing tool cannot be named behaves as before.
214
+ m = proxy.SessionMonitor()
215
+ m.note_tool_result_error(SHELL_SOURCE_READ, None, tool_names=[""])
216
+ self.assertEqual(m.error_signature_streak, 1)
217
+
218
+ def test_ledger_read_incident_never_arms_the_guard(self):
219
+ m = proxy.SessionMonitor()
220
+ for _ in range(4):
221
+ m.note_tool_result_error(SHELL_SOURCE_READ, None, tool_names=["read"])
222
+ body = {"messages": [{"role": "user", "content": "go"}], "tool_choice": "auto"}
223
+ proxy._maybe_inject_error_loop_break(body, m)
224
+ self.assertEqual(m.error_loop_fires, 0)
225
+ self.assertEqual(len(body["messages"]), 1)
226
+
227
+
228
+ class KnownFixCoverageTests(unittest.TestCase):
229
+ """The uncovered ledger classes now carry a KNOWN FIX."""
230
+
231
+ def test_page_not_found(self):
232
+ fix = proxy.loop_class_fix('{"error": "page not found."}')
233
+ self.assertIn("404", fix)
234
+ self.assertIn("CANNOT work", fix)
235
+
236
+ def test_psql_no_password(self):
237
+ fix = proxy.loop_class_fix(WRAPPER_WITH_STDERR)
238
+ self.assertIn("CANNOT work", fix)
239
+ self.assertIn("Do NOT guess passwords", fix)
240
+
241
+ def test_missing_pgcrypto(self):
242
+ fix = proxy.loop_class_fix(
243
+ "error: function gen_random_bytes(integer) does not exist"
244
+ )
245
+ self.assertIn("pgcrypto", fix)
246
+
247
+ def test_decisive_traceback_signature_gets_no_invented_fix(self):
248
+ self.assertEqual(proxy.loop_class_fix("valueerror: boom"), "")
249
+
250
+ def test_original_classes_still_covered(self):
251
+ self.assertIn(
252
+ "byte-for-byte",
253
+ proxy.loop_class_fix(
254
+ "error: edit could not find oldstring in the file. it must match"
255
+ ),
256
+ )
257
+ self.assertIn(
258
+ "does not exist",
259
+ proxy.loop_class_fix(
260
+ 'skill "uap-router__react" not found. available skills: adapt'
261
+ ),
262
+ )
263
+
264
+
265
+ class SecretRedactionTests(unittest.TestCase):
266
+ """Signatures flow into ledger records and tail directives the model
267
+ reads, and error text is UNTRUSTED tool output: credential-shaped
268
+ material must be redacted before it can anchor a signature (security
269
+ review 2026-10-18)."""
270
+
271
+ def test_key_value_secret_never_survives(self):
272
+ sig = proxy._error_signature("error: password=hunter2 rejected")
273
+ self.assertNotIn("hunter2", sig)
274
+ self.assertIn("<secret>", sig)
275
+
276
+ def test_aws_access_key_never_survives(self):
277
+ # Built at runtime: the AWS-docs example key must not sit as one whole
278
+ # literal in the source or the repo's own secret scanner flags the
279
+ # test itself (prepass [aws-access-key], 2026-10-18).
280
+ aws_key = "AKIA" + "IOSFODNN7EXAMPLE"
281
+ sig = proxy._error_signature("error: invalid access key " + aws_key)
282
+ self.assertNotIn(aws_key.lower(), sig)
283
+ self.assertIn("<secret>", sig)
284
+
285
+ def test_url_credentials_never_survive(self):
286
+ sig = proxy._error_signature(
287
+ "fatal: https://admin:hunter2@db.internal:5432 refused"
288
+ )
289
+ self.assertNotIn("hunter2", sig)
290
+ self.assertNotIn("admin", sig)
291
+
292
+ def test_high_entropy_token_never_survives(self):
293
+ token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
294
+ sig = proxy._error_signature("auth failed: bearer " + token)
295
+ self.assertNotIn(token.lower(), sig)
296
+ self.assertIn("<secret>", sig)
297
+
298
+
299
+ class ClockBannerTests(unittest.TestCase):
300
+ """A clock-banner prefix normalizes to a shared "#:#:#" shape that
301
+ unrelated runs would collide on; _first_meaningful_signature strips it
302
+ rather than keying on it (code-quality review 2026-10-18)."""
303
+
304
+ def test_banner_prefix_is_stripped_not_keyed_on(self):
305
+ sig = proxy._first_meaningful_signature(
306
+ "[12:34:56] error: command failed (exit code: 1)\n"
307
+ "[12:34:57] connection refused by peer"
308
+ )
309
+ self.assertEqual(sig, "connection refused by peer")
310
+
311
+ def test_a_line_that_is_only_a_banner_names_nothing(self):
312
+ self.assertEqual(proxy._first_meaningful_signature("[12:34:56]"), "")
313
+ self.assertEqual(proxy._first_meaningful_signature("12:34:56.789"), "")
314
+
315
+
316
+ class DeclaredFlagWrapperTests(unittest.TestCase):
317
+ """The flagged-path fallback must route through the same first-meaningful
318
+ ladder: a declared wrapper-only failure is unnameable and must NOT
319
+ re-manufacture a content-free signature (code-quality review 2026-10-18,
320
+ probed live)."""
321
+
322
+ def test_declared_flag_on_wrapper_only_forms_no_signature(self):
323
+ m = proxy.SessionMonitor()
324
+ m.note_tool_result_error(WRAPPER_ONLY, True, tool_names=["bash"])
325
+ self.assertEqual(m.error_signature_streak, 0)
326
+ self.assertEqual(m.last_error_signature, "")
327
+
328
+ def test_declared_flag_on_wrapper_with_stderr_anchors_on_stderr(self):
329
+ m = proxy.SessionMonitor()
330
+ m.note_tool_result_error(WRAPPER_WITH_STDERR, True, tool_names=["read"])
331
+ self.assertEqual(m.error_signature_streak, 1)
332
+ self.assertTrue(m.last_error_signature.startswith("psql: "))
333
+
334
+ def test_declared_flag_on_wrapper_with_output_anchors_on_output(self):
335
+ m = proxy.SessionMonitor()
336
+ m.note_tool_result_error(WRAPPER_WITH_OUTPUT, True, tool_names=["bash"])
337
+ self.assertEqual(m.last_error_signature, "compiling main.c")
338
+
339
+ def test_declared_unnameable_failure_is_still_owned_by_doubling(self):
340
+ # No signature streak is correct -- but the identical-call shape must
341
+ # stay counted somewhere (architect review 2026-10-18).
342
+ m = proxy.SessionMonitor()
343
+ m.note_doubling_signal("Bash:abcd1234", WRAPPER_ONLY, result_error=True)
344
+ m.note_doubling_signal("Bash:abcd1234", WRAPPER_ONLY, result_error=True)
345
+ self.assertEqual(m.doubling_streak, 2)
346
+
347
+
348
+ if __name__ == "__main__":
349
+ unittest.main()
@@ -16,13 +16,23 @@ import sys
16
16
  import unittest
17
17
  from pathlib import Path
18
18
 
19
+ # Shared resolution: the data layer lives at the MAIN checkout root (shared
20
+ # across worktrees) — same module the CLI itself uses.
21
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "UAP"))
22
+ from paths import main_checkout_root
23
+
19
24
 
20
25
  class TestUAPCompliance(unittest.TestCase):
21
26
  """Test suite for UAP protocol compliance."""
22
27
 
23
28
  def setUp(self):
24
29
  """Setup test fixtures."""
25
- self.project_root = Path(__file__).parent.parent.parent
30
+ # The pre-reorg depth arithmetic resolved to tools/ (pointing the
31
+ # suite at a nonexistent tools/agents/data/); and a worktree copy
32
+ # of this file used to look inside the worktree, where no data
33
+ # exists. Resolve the MAIN checkout root instead — the shared data
34
+ # layer the session-start hook and the TS CLI both use.
35
+ self.project_root = main_checkout_root(Path(__file__))
26
36
  self.db_path = self.project_root / "agents/data/memory/short_term.db"
27
37
  self.coord_db_path = (
28
38
  self.project_root / "agents/data/coordination/coordination.db"
package/web/dash/core.js CHANGED
@@ -18,6 +18,7 @@
18
18
  { id: 'deliver', label: 'Deliver' },
19
19
  { id: 'policies', label: 'Policies' },
20
20
  { id: 'models', label: 'Models' },
21
+ { id: 'clients', label: 'Clients' },
21
22
  { id: 'memory', label: 'Memory' },
22
23
  ];
23
24