@openchambery/web 2.0.0-beta.1 → 2.0.0-beta.4

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 (287) hide show
  1. package/README.md +15 -11
  2. package/bin/cli-opencode.test.js +4 -4
  3. package/bin/cli.js +1 -1
  4. package/bin/lib/cli-startup.js +1 -1
  5. package/bin/lib/cli-startup.test.js +18 -4
  6. package/dist/assets/{AgentAvatar-CLWWNu_Q.js → AgentAvatar-Q1SmFRx2.js} +1 -1
  7. package/dist/assets/AssistantView-CwXkiCAd.js +3 -0
  8. package/dist/assets/{DiagramView-xuSJTnv7.js → DiagramView-BW92schO.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-Ov0D96d0.js → FadeInOnReveal-BZmwl-zE.js} +1 -1
  10. package/dist/assets/{Icon-DD-BuMfZ.js → Icon-AQPvxMiU.js} +1 -1
  11. package/dist/assets/{MarkdownRenderer-bc_p1H7l.js → MarkdownRenderer-Bv05iDZZ.js} +2 -2
  12. package/dist/assets/{MarkdownRendererImpl-SKi4oG0c.js → MarkdownRendererImpl-C81xqYib.js} +3 -3
  13. package/dist/assets/MarkstreamRendererImpl-CXUCQuKJ.js +3 -0
  14. package/dist/assets/{MobileDetailNavigation-Bnx59U_J.js → MobileDetailNavigation-BMgIu7zc.js} +1 -1
  15. package/dist/assets/{MobileShareBridge-BQlP4J-F.js → MobileShareBridge-BOXGNHmC.js} +2 -2
  16. package/dist/assets/MobileSurface-xuc_z0_G.js +2 -0
  17. package/dist/assets/{MultiRunWindow-ChZGob0v.js → MultiRunWindow-B5At2Z95.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-DtQJxZG-.js → OnboardingScreen-bsPLgz4r.js} +1 -1
  19. package/dist/assets/OverlayScrollbar-4wp4migh.js +2 -0
  20. package/dist/assets/SettingsView-CJ6tcH5_.js +25 -0
  21. package/dist/assets/{SettingsWindow-DfnsFa8y.js → SettingsWindow-CMtX2bMl.js} +1 -1
  22. package/dist/assets/{TerminalView-Dh9MWCpS.js → TerminalView-Dvr-ysTS.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-BejmjrRk.js → ToolOutputDialog-uH1ZZDSS.js} +2 -2
  24. package/dist/assets/appThemeRegistry-DK7q2juD.js +1 -0
  25. package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
  26. package/dist/assets/{button-D-rATN7w.js → button-CcNJUxUd.js} +1 -1
  27. package/dist/assets/collapsible-ZmZ6rLWs.js +3 -0
  28. package/dist/assets/context-menu-B2LXJgXL.js +1 -0
  29. package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-C6HrP0jq.js} +1 -1
  30. package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DdVBp45U.js} +1 -1
  31. package/dist/assets/{diagram-DQf7Y_uR.js → diagram-CxTs9hCQ.js} +1 -1
  32. package/dist/assets/{dialog-bfbvR4VH.js → dialog-KMVL9QKF.js} +1 -1
  33. package/dist/assets/es-CN3E_zXj.js +15 -0
  34. package/dist/assets/{fileMentionTouchSelection-BvrNCroG.js → fileMentionTouchSelection-7RIwlpkB.js} +1 -1
  35. package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-CjdBRy8y.js} +1 -1
  36. package/dist/assets/fr-9zZvQZD2.js +13 -0
  37. package/dist/assets/gitApi-yuWg9BN4.js +10 -0
  38. package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-9vQBq3zp.js} +1 -1
  39. package/dist/assets/{input-8r6F_Fza.js → input-CrIvk3HZ.js} +1 -1
  40. package/dist/assets/insertionBoundaries-BiUCSkx-.js +1 -0
  41. package/dist/assets/ja-Dnjk_82N.js +15 -0
  42. package/dist/assets/ko-BxCEE99C.js +15 -0
  43. package/dist/assets/{main-CCE_4y5F.js → main-C9mIBo77.js} +2 -2
  44. package/dist/assets/main-DiL6w6KF.js +169 -0
  45. package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
  46. package/dist/assets/{markdownHeightCache-1Qo6y6IJ.js → markdownHeightCache-BmXVY_gc.js} +1 -1
  47. package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
  48. package/dist/assets/mcpQueries-C7jknHV8.js +1 -0
  49. package/dist/assets/miniChat-CwtUaElU.js +2 -0
  50. package/dist/assets/mobile-DB-6--ws.js +2 -0
  51. package/dist/assets/multirun-TAsxFI8e.js +4 -0
  52. package/dist/assets/{persistence-Ce3AnpEr.js → persistence-56kq8Z4V.js} +2 -2
  53. package/dist/assets/pl-CNNsWnPM.js +15 -0
  54. package/dist/assets/platform-C13fMVEs.js +6 -0
  55. package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
  56. package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
  57. package/dist/assets/renderElectronMiniChatApp-CGj0MNRU.js +2 -0
  58. package/dist/assets/renderMobileApp-CGMMqbsO.js +28 -0
  59. package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
  60. package/dist/assets/{runtimeConfig-BMvqa5we.js → runtimeConfig-BEANkvOd.js} +1 -1
  61. package/dist/assets/runtimeEndpointReset-A_ZBUJFS.js +147 -0
  62. package/dist/assets/sessionChangeSummary-qOLrP9fw.js +2 -0
  63. package/dist/assets/{sessionLookup-gh-hVRIc.js → sessionLookup-34XFmP9C.js} +1 -1
  64. package/dist/assets/{sortable-tabs-strip-CudoSgV4.js → sortable-tabs-strip-DyEV0Heg.js} +1 -1
  65. package/dist/assets/src-ubCWXZhR.css +2 -0
  66. package/dist/assets/store-DLCiZaA6.js +15 -0
  67. package/dist/assets/{terminalApi-DiF6JmBP.js → terminalApi-CXaBEoa_.js} +1 -1
  68. package/dist/assets/textarea-BBOlRI9O.js +1 -0
  69. package/dist/assets/{tooltip-CQvpK-UV.js → tooltip-CPI4fr1a.js} +1 -1
  70. package/dist/assets/uk-rwQrxeYM.js +15 -0
  71. package/dist/assets/{url-_z7evzdK.js → url-DRDQXjyX.js} +1 -1
  72. package/dist/assets/{useAppFontEffects-Cpmpo7Wb.js → useAppFontEffects-CiDe42I5.js} +57 -43
  73. package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
  74. package/dist/assets/useDesktopWindowControlsLayout-BlizAlOt.js +1 -0
  75. package/dist/assets/useEffectiveDirectory-C_CouL5v.js +1 -0
  76. package/dist/assets/{useFontPreferences-DcbXQOkc.js → useFontPreferences-B0oUZAiB.js} +1 -1
  77. package/dist/assets/useMobileNavigationStore-DfPClaW-.js +414 -0
  78. package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
  79. package/dist/assets/useSessionAutoCleanup-DtrfbxAp.js +2 -0
  80. package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
  81. package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
  82. package/dist/assets/{useWorkerHighlightedLines-Bqn4AiBL.js → useWorkerHighlightedLines-CPNYz6Wf.js} +2 -2
  83. package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
  84. package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
  85. package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
  86. package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
  87. package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
  88. package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
  89. package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
  90. package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
  91. package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
  92. package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
  93. package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
  94. package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
  95. package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
  96. package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
  97. package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
  98. package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
  99. package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
  100. package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
  101. package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
  102. package/dist/index.html +13 -13
  103. package/dist/mini-chat.html +13 -13
  104. package/dist/mobile.html +13 -13
  105. package/package.json +1 -1
  106. package/server/index.js +327 -62
  107. package/server/lib/assistants/DOCUMENTATION.md +14 -9
  108. package/server/lib/assistants/contracts.js +11 -1
  109. package/server/lib/assistants/projection-history.test.js +265 -18
  110. package/server/lib/assistants/routes.js +6 -0
  111. package/server/lib/assistants/service.js +417 -231
  112. package/server/lib/assistants/service.test.js +392 -148
  113. package/server/lib/conversations/DOCUMENTATION.md +1 -1
  114. package/server/lib/conversations/service.js +1 -0
  115. package/server/lib/conversations/service.test.js +16 -0
  116. package/server/lib/conversations/validation.js +11 -1
  117. package/server/lib/event-stream/DOCUMENTATION.md +3 -1
  118. package/server/lib/event-stream/directory-ws-bridge.js +13 -2
  119. package/server/lib/event-stream/global-hub.js +49 -4
  120. package/server/lib/event-stream/reasoning-projection.js +16 -1
  121. package/server/lib/event-stream/reasoning-projection.test.js +15 -0
  122. package/server/lib/event-stream/runtime.js +57 -20
  123. package/server/lib/event-stream/runtime.test.js +112 -0
  124. package/server/lib/llm/DOCUMENTATION.md +33 -9
  125. package/server/lib/llm/catalog.js +1 -1
  126. package/server/lib/llm/completions.js +5 -1
  127. package/server/lib/llm/completions.test.js +3 -3
  128. package/server/lib/llm/generate.js +237 -145
  129. package/server/lib/llm/generate.test.js +201 -46
  130. package/server/lib/llm/routes.js +2 -0
  131. package/server/lib/message-queue/runtime.js +15 -2
  132. package/server/lib/notifications/runtime.js +23 -5
  133. package/server/lib/notifications/runtime.test.js +11 -0
  134. package/server/lib/opencode/DOCUMENTATION.md +85 -17
  135. package/server/lib/opencode/agent-document.d.ts +28 -0
  136. package/server/lib/opencode/agent-document.js +280 -0
  137. package/server/lib/opencode/agent-document.test.js +137 -0
  138. package/server/lib/opencode/agents.js +96 -0
  139. package/server/lib/opencode/auth-state-runtime.js +5 -0
  140. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  141. package/server/lib/opencode/config-entity-routes.js +42 -99
  142. package/server/lib/opencode/config-entity-routes.test.js +107 -0
  143. package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
  144. package/server/lib/opencode/ensure-cli.js +1 -2
  145. package/server/lib/opencode/env-runtime.js +40 -26
  146. package/server/lib/opencode/env-runtime.test.js +10 -17
  147. package/server/lib/opencode/feature-routes-runtime.js +43 -4
  148. package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
  149. package/server/lib/opencode/http-server-connections.js +98 -0
  150. package/server/lib/opencode/http-server-connections.test.js +321 -0
  151. package/server/lib/opencode/lifecycle.js +349 -39
  152. package/server/lib/opencode/lifecycle.test.js +322 -0
  153. package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
  154. package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
  155. package/server/lib/opencode/openchamber-routes.js +7 -1
  156. package/server/lib/opencode/opencode2-pin.js +4 -4
  157. package/server/lib/opencode/opencode2-pin.test.js +1 -0
  158. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +127 -4
  159. package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
  160. package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
  161. package/server/lib/opencode/plugin-routes.js +3 -18
  162. package/server/lib/opencode/plugin-routes.test.js +9 -9
  163. package/server/lib/opencode/provider-catalog.js +58 -12
  164. package/server/lib/opencode/provider-catalog.test.js +48 -0
  165. package/server/lib/opencode/proxy.js +252 -46
  166. package/server/lib/opencode/proxy.test.js +235 -2
  167. package/server/lib/opencode/routes.js +296 -62
  168. package/server/lib/opencode/routes.test.js +278 -12
  169. package/server/lib/opencode/runtime-contract.js +401 -0
  170. package/server/lib/opencode/runtime-contract.test.js +214 -0
  171. package/server/lib/opencode/server-opencode-fetch.js +242 -0
  172. package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
  173. package/server/lib/opencode/server-utils-runtime.js +33 -0
  174. package/server/lib/opencode/shared-service.integration.test.js +66 -0
  175. package/server/lib/opencode/shared-service.js +147 -0
  176. package/server/lib/opencode/shared-service.test.js +93 -0
  177. package/server/lib/opencode/shared.js +3 -3
  178. package/server/lib/opencode/shutdown-runtime.js +15 -6
  179. package/server/lib/opencode/shutdown-runtime.test.js +49 -1
  180. package/server/lib/opencode/skill-routes.js +20 -33
  181. package/server/lib/opencode/skill-routes.test.js +35 -2
  182. package/server/lib/opencode/skills.test.js +15 -0
  183. package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
  184. package/server/lib/opencode/static-routes-runtime.js +3 -1
  185. package/server/lib/opencode/static-routes-runtime.test.js +33 -0
  186. package/server/lib/opencode/v2-client.js +14 -3
  187. package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
  188. package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
  189. package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
  190. package/server/lib/permission-auto-accept/runtime.js +14 -7
  191. package/server/lib/permission-auto-accept/runtime.test.js +27 -12
  192. package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
  193. package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -0
  194. package/server/lib/scheduled-tasks/runtime.js +52 -4
  195. package/server/lib/scheduled-tasks/runtime.test.js +80 -1
  196. package/server/lib/session-goal/DOCUMENTATION.md +113 -13
  197. package/server/lib/session-goal/routes.js +9 -0
  198. package/server/lib/session-goal/runtime.js +1579 -117
  199. package/server/lib/session-goal/runtime.test.js +1453 -84
  200. package/server/lib/session-index/DOCUMENTATION.md +18 -15
  201. package/server/lib/session-index/event-ingest.js +89 -16
  202. package/server/lib/session-index/event-ingest.test.js +73 -0
  203. package/server/lib/session-index/host-authority-snapshot.js +92 -0
  204. package/server/lib/session-index/routes.host-archive.test.js +201 -0
  205. package/server/lib/session-index/routes.js +63 -10
  206. package/server/lib/session-index/routes.test.js +8 -8
  207. package/server/lib/session-index/service.js +7 -1
  208. package/server/lib/session-index/sync-runtime.js +161 -34
  209. package/server/lib/session-index/sync-runtime.test.js +155 -1
  210. package/server/lib/session-metadata/DOCUMENTATION.md +61 -16
  211. package/server/lib/session-metadata/routes.js +108 -9
  212. package/server/lib/session-metadata/routes.test.js +47 -2
  213. package/server/lib/session-metadata/session-archive.d.ts +73 -0
  214. package/server/lib/session-metadata/session-archive.js +654 -0
  215. package/server/lib/session-metadata/session-archive.test.js +359 -0
  216. package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
  217. package/server/lib/session-metadata/session-metadata-store.js +212 -72
  218. package/server/lib/session-metadata/session-metadata-store.test.js +205 -10
  219. package/server/lib/session-metadata/session-projection.d.ts +41 -0
  220. package/server/lib/session-metadata/session-projection.js +255 -0
  221. package/server/lib/session-metadata/session-projection.test.js +64 -0
  222. package/server/lib/session-metadata/system-session.js +55 -0
  223. package/server/lib/session-metadata/system-session.test.js +55 -0
  224. package/server/lib/session-title/DOCUMENTATION.md +40 -7
  225. package/server/lib/session-title/native-runtime.test.js +167 -0
  226. package/server/lib/session-title/runtime.js +67 -42
  227. package/server/lib/session-title/session-access.js +46 -0
  228. package/server/lib/session-title/session-access.test.js +91 -0
  229. package/server/lib/session-turn-pages/DOCUMENTATION.md +9 -8
  230. package/server/lib/session-turn-pages/routes.js +17 -102
  231. package/server/lib/session-turn-pages/routes.test.js +281 -0
  232. package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
  233. package/server/lib/session-turn-pages/session-message-projection.js +281 -0
  234. package/server/lib/small-model/DOCUMENTATION.md +101 -147
  235. package/server/lib/small-model/catalog.js +31 -51
  236. package/server/lib/small-model/catalog.test.js +65 -10
  237. package/server/lib/small-model/custom-api.js +68 -0
  238. package/server/lib/small-model/custom-api.test.js +64 -0
  239. package/server/lib/small-model/index.js +124 -240
  240. package/server/lib/small-model/resolve.js +35 -120
  241. package/server/lib/small-model/resolve.test.js +20 -110
  242. package/server/lib/small-model/routes.js +1 -3
  243. package/server/lib/small-model/summary-settings.test.js +157 -253
  244. package/server/opencode-proxy.test.js +2 -0
  245. package/dist/assets/AssistantView-BTCkm8yk.js +0 -3
  246. package/dist/assets/MarkstreamRendererImpl-CnkodGJW.js +0 -3
  247. package/dist/assets/MobileSurface-BXLeiZ4d.js +0 -2
  248. package/dist/assets/OverlayScrollbar-pMwxaCca.js +0 -2
  249. package/dist/assets/SettingsView-B5I0KDTZ.js +0 -25
  250. package/dist/assets/appThemeRegistry-COGfxTFV.js +0 -1
  251. package/dist/assets/collapsible-BXRd5_Hr.js +0 -1
  252. package/dist/assets/context-menu-jGtH6fR9.js +0 -1
  253. package/dist/assets/es-c52LTHHe.js +0 -15
  254. package/dist/assets/fr-PisFbUE8.js +0 -13
  255. package/dist/assets/gitApi-ButwzQ0N.js +0 -9
  256. package/dist/assets/insertionBoundaries-DGs7S5zv.js +0 -1
  257. package/dist/assets/ja-C8Ikun3Z.js +0 -15
  258. package/dist/assets/ko-ClFys5o2.js +0 -15
  259. package/dist/assets/main-Dn9rpsgl.js +0 -169
  260. package/dist/assets/markdown-worker-CptoICmq.js +0 -2
  261. package/dist/assets/miniChat-BpjxVqgg.js +0 -2
  262. package/dist/assets/mobile-nMJCaNq1.js +0 -2
  263. package/dist/assets/multirun-BvSG94mW.js +0 -4
  264. package/dist/assets/pl-CnaKWN5F.js +0 -15
  265. package/dist/assets/platform-CWTIp02q.js +0 -4
  266. package/dist/assets/projectMeta-DOe49dM_.js +0 -3
  267. package/dist/assets/pt-BR-s8OpfRI6.js +0 -15
  268. package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +0 -2
  269. package/dist/assets/renderMobileApp-y-S8y8v_.js +0 -28
  270. package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
  271. package/dist/assets/runtimeEndpointReset-B6BxhvR0.js +0 -147
  272. package/dist/assets/sessionChangeSummary-VVBlEXuh.js +0 -2
  273. package/dist/assets/src-BzX5UI-c.css +0 -2
  274. package/dist/assets/store-DJczmczv.js +0 -15
  275. package/dist/assets/textarea-3XR8sUfy.js +0 -1
  276. package/dist/assets/uk-CaX0iqkB.js +0 -15
  277. package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +0 -1
  278. package/dist/assets/useEffectiveDirectory-CN6JoCFw.js +0 -1
  279. package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +0 -414
  280. package/dist/assets/useSessionAutoCleanup-BLjymaFt.js +0 -2
  281. package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
  282. package/dist/assets/zh-CN-20MYEjha.js +0 -15
  283. package/dist/assets/zh-TW-DiN04qTw.js +0 -15
  284. package/server/lib/small-model/call.js +0 -427
  285. package/server/lib/small-model/call.test.js +0 -382
  286. package/server/lib/small-model/opencode-session.js +0 -183
  287. package/server/lib/small-model/opencode-session.test.js +0 -147
@@ -0,0 +1,401 @@
1
+ /**
2
+ * Runtime contract admission for the actual OpenCode serve in use.
3
+ *
4
+ * Discovery may still prefer a global CLI / existing serve. Feature open is
5
+ * based on the running serve version against a documented matrix — not on
6
+ * "any 2.x health body" alone. Health success is reachability, not full
7
+ * execution semantics.
8
+ *
9
+ * Evidence bounds (ticket 11):
10
+ * - MIN_VERIFIED: pin / installed verification (`2.0.12`)
11
+ * Any acceptable OpenCode 2.x at or above MIN_VERIFIED may execute. Below-min
12
+ * 2.x and 1.x are refused with upgrade guidance; there is no upper version cap.
13
+ */
14
+
15
+ import {
16
+ PINNED_OPENCODE2_VERSION,
17
+ compareOpenCode2Versions,
18
+ isAcceptableOpenCode2HealthVersion,
19
+ isOpenCode1xVersion,
20
+ } from './opencode2-pin.js';
21
+
22
+ /** Lowest version with documented OpenChamber + OpenCode 2 contract tests. */
23
+ export const RUNTIME_CONTRACT_MIN_VERIFIED = PINNED_OPENCODE2_VERSION;
24
+
25
+ /**
26
+ * Optional / core capabilities and the minimum serve version that admits them.
27
+ * Tickets 01 / 02 / 05 / 06 share the verified floor until narrower evidence exists.
28
+ */
29
+ export const RUNTIME_CONTRACT_CAPABILITIES = Object.freeze({
30
+ 'core.protocol': {
31
+ minVersion: RUNTIME_CONTRACT_MIN_VERIFIED,
32
+ requiredForExecution: true,
33
+ description: 'Core HTTP/SSE session protocol used by OpenChamber',
34
+ },
35
+ 'session.prompt': {
36
+ minVersion: RUNTIME_CONTRACT_MIN_VERIFIED,
37
+ requiredForExecution: true,
38
+ description: 'Prompt admission and idle send (ticket 02)',
39
+ },
40
+ 'session.queuedInput': {
41
+ minVersion: RUNTIME_CONTRACT_MIN_VERIFIED,
42
+ requiredForExecution: false,
43
+ description: 'Queued input selection / inheritance (ticket 01)',
44
+ },
45
+ 'session.background': {
46
+ minVersion: RUNTIME_CONTRACT_MIN_VERIFIED,
47
+ requiredForExecution: false,
48
+ description: 'Background / non-blocking work (ticket 05)',
49
+ },
50
+ 'session.generationFallback': {
51
+ minVersion: RUNTIME_CONTRACT_MIN_VERIFIED,
52
+ requiredForExecution: false,
53
+ description: 'Session generation fallback helpers (ticket 06)',
54
+ },
55
+ 'migration.v1Read': {
56
+ minVersion: RUNTIME_CONTRACT_MIN_VERIFIED,
57
+ requiredForExecution: false,
58
+ description: 'Read-only V1 migration status probe',
59
+ },
60
+ });
61
+
62
+ /**
63
+ * @param {unknown} value
64
+ * @returns {string | null}
65
+ */
66
+ export function normalizeRuntimeVersion(value) {
67
+ if (typeof value !== 'string') return null;
68
+ const trimmed = value.trim().replace(/^v/i, '');
69
+ return trimmed || null;
70
+ }
71
+
72
+ /**
73
+ * @param {string | null | undefined} version
74
+ * @returns {'1x' | 'invalid' | 'below-min' | 'verified' | 'unknown'}
75
+ */
76
+ export function classifyRuntimeVersionBand(version) {
77
+ const normalized = normalizeRuntimeVersion(version);
78
+ if (!normalized) return 'unknown';
79
+ if (isOpenCode1xVersion(normalized)) return '1x';
80
+ if (!isAcceptableOpenCode2HealthVersion(normalized)) return 'invalid';
81
+ if (compareOpenCode2Versions(normalized, RUNTIME_CONTRACT_MIN_VERIFIED) < 0) {
82
+ return 'below-min';
83
+ }
84
+ return 'verified';
85
+ }
86
+
87
+ /**
88
+ * @param {string | null | undefined} version
89
+ * @param {string} minimum
90
+ */
91
+ export function versionMeetsMinimum(version, minimum) {
92
+ const normalized = normalizeRuntimeVersion(version);
93
+ if (!normalized || !isAcceptableOpenCode2HealthVersion(normalized)) return false;
94
+ return compareOpenCode2Versions(normalized, minimum) >= 0;
95
+ }
96
+
97
+ /**
98
+ * @param {{
99
+ * serveVersion?: string | null,
100
+ * cliVersion?: string | null,
101
+ * reachable?: boolean,
102
+ * authenticated?: boolean | null,
103
+ * healthOk?: boolean | null,
104
+ * migrationAdmitTranscript?: boolean | null,
105
+ * migrationPhase?: string | null,
106
+ * migrationError?: string | null,
107
+ * }} input
108
+ */
109
+ export function evaluateRuntimeContract(input = {}) {
110
+ const serveVersion = normalizeRuntimeVersion(input.serveVersion);
111
+ const cliVersion = normalizeRuntimeVersion(input.cliVersion);
112
+ const reachable = input.reachable === true;
113
+ const authenticated = typeof input.authenticated === 'boolean' ? input.authenticated : null;
114
+ const healthOk = typeof input.healthOk === 'boolean' ? input.healthOk : null;
115
+ const migrationAdmit = typeof input.migrationAdmitTranscript === 'boolean'
116
+ ? input.migrationAdmitTranscript
117
+ : null;
118
+ const migrationPhase = typeof input.migrationPhase === 'string' ? input.migrationPhase : null;
119
+ const migrationError = typeof input.migrationError === 'string' ? input.migrationError : null;
120
+
121
+ /** @type {string[]} */
122
+ const reasons = [];
123
+ const versionBand = classifyRuntimeVersionBand(serveVersion);
124
+ const versionMismatch = Boolean(
125
+ serveVersion
126
+ && cliVersion
127
+ && serveVersion !== cliVersion,
128
+ );
129
+
130
+ if (!reachable) {
131
+ reasons.push('unreachable');
132
+ }
133
+ if (authenticated === false) {
134
+ reasons.push('auth-failed');
135
+ }
136
+ if (healthOk === false) {
137
+ reasons.push('health-failed');
138
+ }
139
+ if (!serveVersion) {
140
+ reasons.push('serve-version-unknown');
141
+ } else if (versionBand === '1x') {
142
+ reasons.push('1x-version');
143
+ } else if (versionBand === 'invalid') {
144
+ reasons.push('invalid-version');
145
+ } else if (versionBand === 'below-min') {
146
+ reasons.push('below-min-verified');
147
+ }
148
+ if (versionMismatch) {
149
+ reasons.push('cli-serve-version-mismatch');
150
+ }
151
+
152
+ /** @type {Record<string, { available: boolean, reason: string | null, minVersion: string, requiredForExecution: boolean }>} */
153
+ const capabilities = {};
154
+ for (const [id, meta] of Object.entries(RUNTIME_CONTRACT_CAPABILITIES)) {
155
+ let available = false;
156
+ /** @type {string | null} */
157
+ let reason = null;
158
+ if (!reachable) {
159
+ reason = 'unreachable';
160
+ } else if (authenticated === false) {
161
+ reason = 'auth-failed';
162
+ } else if (healthOk === false) {
163
+ reason = 'health-failed';
164
+ } else if (!serveVersion) {
165
+ reason = 'serve-version-unknown';
166
+ } else if (versionBand === '1x') {
167
+ reason = '1x-version';
168
+ } else if (versionBand === 'invalid') {
169
+ reason = 'invalid-version';
170
+ } else if (!versionMeetsMinimum(serveVersion, meta.minVersion)) {
171
+ reason = 'below-capability-min';
172
+ } else {
173
+ available = true;
174
+ }
175
+ capabilities[id] = {
176
+ available,
177
+ reason,
178
+ minVersion: meta.minVersion,
179
+ requiredForExecution: meta.requiredForExecution === true,
180
+ };
181
+ }
182
+
183
+ const protocolCompatible = reachable
184
+ && authenticated !== false
185
+ && healthOk !== false
186
+ && versionBand === 'verified';
187
+
188
+ const requiredCapsOk = Object.values(capabilities)
189
+ .filter((cap) => cap.requiredForExecution)
190
+ .every((cap) => cap.available);
191
+
192
+ const migrationExecutable = migrationAdmit === null
193
+ ? null
194
+ : migrationAdmit === true;
195
+
196
+ if (migrationAdmit === false) {
197
+ reasons.push(migrationPhase === 'error' ? 'migration-error' : 'migration-blocked');
198
+ }
199
+
200
+ const executionAllowed = reachable
201
+ && authenticated !== false
202
+ && healthOk !== false
203
+ && protocolCompatible
204
+ && requiredCapsOk
205
+ && migrationAdmit !== false
206
+ && versionBand === 'verified';
207
+
208
+ /** @type {string} */
209
+ let phase = 'ready';
210
+ if (!reachable) {
211
+ phase = 'unreachable';
212
+ } else if (authenticated === false) {
213
+ phase = 'unauthenticated';
214
+ } else if (!serveVersion || versionBand === 'invalid' || versionBand === 'unknown') {
215
+ phase = 'unknown';
216
+ } else if (versionBand === '1x' || versionBand === 'below-min' || !protocolCompatible) {
217
+ phase = 'incompatible';
218
+ } else if (migrationAdmit === false) {
219
+ phase = 'migration-blocked';
220
+ } else if (healthOk === false) {
221
+ phase = 'unhealthy';
222
+ }
223
+
224
+ return {
225
+ schemaVersion: 1,
226
+ phase,
227
+ reachable,
228
+ authenticated,
229
+ healthOk,
230
+ protocolCompatible,
231
+ executionAllowed,
232
+ migrationExecutable,
233
+ migrationPhase,
234
+ migrationError,
235
+ serveVersion,
236
+ cliVersion,
237
+ versionBand,
238
+ versionMismatch,
239
+ minVerifiedVersion: RUNTIME_CONTRACT_MIN_VERIFIED,
240
+ maxVerifiedVersion: null,
241
+ pinnedVersion: PINNED_OPENCODE2_VERSION,
242
+ capabilities,
243
+ reasons,
244
+ };
245
+ }
246
+
247
+ /**
248
+ * Whether a proxied OpenCode API path is diagnostic-only (must stay available
249
+ * when execution is limited).
250
+ * @param {string} method
251
+ * @param {string} pathWithQuery
252
+ */
253
+ export function isRuntimeContractDiagnosticPath(method, pathWithQuery) {
254
+ const upper = String(method || 'GET').toUpperCase();
255
+ if (upper !== 'GET' && upper !== 'HEAD') return false;
256
+ const pathOnly = String(pathWithQuery || '').split('?')[0] || '';
257
+ const normalized = pathOnly.replace(/\/+$/, '') || '/';
258
+ return (
259
+ normalized === '/api/info'
260
+ || normalized === '/api/health'
261
+ || normalized === '/global/health'
262
+ || normalized === '/api/experimental/migration/v1'
263
+ || normalized.endsWith('/info')
264
+ || normalized.endsWith('/health')
265
+ );
266
+ }
267
+
268
+ /**
269
+ * Stop / cancel paths that must remain available when execution is limited
270
+ * (user can still halt in-flight work and read diagnostics).
271
+ * @param {string} method
272
+ * @param {string} pathWithQuery
273
+ */
274
+ export function isRuntimeContractStopPath(method, pathWithQuery) {
275
+ const upper = String(method || 'GET').toUpperCase();
276
+ if (upper === 'GET' || upper === 'HEAD' || upper === 'OPTIONS') return false;
277
+ const pathOnly = String(pathWithQuery || '').split('?')[0] || '';
278
+ return /\/session(?:\/[^/]+)?\/(?:interrupt|abort)\b/.test(pathOnly);
279
+ }
280
+
281
+ /**
282
+ * Mutating session / turn paths that require executionAllowed.
283
+ * Interrupt/abort are excluded so stop-task remains available under limited execution.
284
+ * @param {string} method
285
+ * @param {string} pathWithQuery
286
+ */
287
+ export function isRuntimeContractExecutionPath(method, pathWithQuery) {
288
+ const upper = String(method || 'GET').toUpperCase();
289
+ if (upper === 'GET' || upper === 'HEAD' || upper === 'OPTIONS') return false;
290
+ if (isRuntimeContractStopPath(method, pathWithQuery)) return false;
291
+ const pathOnly = String(pathWithQuery || '').split('?')[0] || '';
292
+ // Official v2 turn + control surfaces (and classic aliases). Reads stay open.
293
+ return /\/session(?:\/[^/]+)?\/(?:prompt|prompt_async|command|shell|revert|unrevert|summarize|fork|generate|background|model|agent|synthetic|compact|skill|move|environment|instructions)(?:\/|$)/.test(pathOnly)
294
+ || ((upper === 'POST' || upper === 'PATCH') && /\/session\/[^/]+\/inbox\/[^/]+(?:\/(?:steer|queue))?\/?$/.test(pathOnly))
295
+ || (upper === 'POST' && /\/session\/[^/]+\/form(?:\/|$)/.test(pathOnly))
296
+ || /\/session(?:\/[^/]+)?\/message(?:\/|$)/.test(pathOnly)
297
+ || /\/permission\//.test(pathOnly)
298
+ || /\/question\//.test(pathOnly)
299
+ // Session create / patch title metadata used by host background (queue/goal/scheduled).
300
+ || (upper === 'POST' && /\/session\/?$/.test(pathOnly))
301
+ || (upper === 'PATCH' && /\/session\/[^/]+\/?$/.test(pathOnly));
302
+ }
303
+
304
+ /**
305
+ * Pending / revoked snapshot published while a new serve instance is starting
306
+ * or after start/restart/target change invalidates the previous permit.
307
+ * @param {string} [reason]
308
+ * @param {{ instanceGeneration?: number | null }} [extra]
309
+ */
310
+ export function createRevokedRuntimeContract(reason = 'instance-revoked', extra = {}) {
311
+ const base = evaluateRuntimeContract({
312
+ reachable: false,
313
+ authenticated: null,
314
+ healthOk: null,
315
+ serveVersion: null,
316
+ });
317
+ return {
318
+ ...base,
319
+ phase: 'pending',
320
+ executionAllowed: false,
321
+ reasons: reason ? [reason, ...base.reasons.filter((entry) => entry !== 'unreachable')] : base.reasons,
322
+ instanceGeneration: typeof extra.instanceGeneration === 'number' ? extra.instanceGeneration : null,
323
+ };
324
+ }
325
+
326
+ /**
327
+ * Whether a host-side OpenCode request should be blocked by contract admission.
328
+ * Diagnostics, reads, and stop-task stay available when execution is limited.
329
+ *
330
+ * Production default: execution paths require the **current** contract to set
331
+ * `executionAllowed === true` explicitly. A null/missing contract blocks writes
332
+ * (no silent bypass during startup or after revoke). Unit/DI factories may pass
333
+ * `{ allowMissingContract: true }` when no contract is wired.
334
+ *
335
+ * @param {string} method
336
+ * @param {string} pathWithQuery
337
+ * @param {{ executionAllowed?: boolean } | null | undefined} contract
338
+ * @param {{ allowMissingContract?: boolean }} [options]
339
+ */
340
+ export function shouldBlockRuntimeContractExecution(method, pathWithQuery, contract, options = {}) {
341
+ if (isRuntimeContractDiagnosticPath(method, pathWithQuery)) return false;
342
+ if (isRuntimeContractStopPath(method, pathWithQuery)) return false;
343
+ if (!isRuntimeContractExecutionPath(method, pathWithQuery)) return false;
344
+
345
+ if (contract == null || typeof contract !== 'object') {
346
+ // Test DI only: production never opts into missing-contract bypass.
347
+ return options.allowMissingContract !== true;
348
+ }
349
+ // Explicit true only — undefined / false / pending all block execution paths.
350
+ return contract.executionAllowed !== true;
351
+ }
352
+
353
+ /**
354
+ * User-facing message when execution is blocked by version admission.
355
+ * @param {object | null | undefined} contract
356
+ */
357
+ export function formatRuntimeContractBlockedError(contract) {
358
+ const serve = contract?.serveVersion ?? null;
359
+ const min = contract?.minVerifiedVersion ?? RUNTIME_CONTRACT_MIN_VERIFIED;
360
+ const pinned = contract?.pinnedVersion ?? PINNED_OPENCODE2_VERSION;
361
+ const reasons = Array.isArray(contract?.reasons) ? contract.reasons : [];
362
+
363
+ if (reasons.includes('below-min-verified')) {
364
+ const current = serve ? `OpenCode ${serve}` : 'This OpenCode version';
365
+ return `${current} is below the minimum supported version (${min}). Upgrade to ${min} or newer (recommended: ${pinned}). Open Settings → About to update in OpenChamber, or upgrade your global \`opencode\` CLI if you manage it manually.`;
366
+ }
367
+ if (reasons.includes('1x-version')) {
368
+ return `OpenCode 1.x is not supported. Install OpenCode 2 (${min} or newer; recommended: ${pinned}).`;
369
+ }
370
+ if (reasons.includes('invalid-version') || reasons.includes('serve-version-unknown')) {
371
+ return `OpenCode version could not be verified. Install OpenCode 2 (${min} or newer; recommended: ${pinned}) and restart the runtime.`;
372
+ }
373
+ if (reasons.includes('migration-blocked') || reasons.includes('migration-error')) {
374
+ return 'OpenCode is finishing V1 history migration. Wait for migration to complete, then try again.';
375
+ }
376
+ if (reasons.includes('auth-failed')) {
377
+ return 'OpenCode authentication failed. Check OpenCode credentials in Settings and restart the runtime.';
378
+ }
379
+ if (reasons.includes('health-failed') || reasons.includes('unreachable')) {
380
+ return 'OpenCode is not reachable. Restart OpenCode from Settings → About or check that the serve process is running.';
381
+ }
382
+ return 'OpenCode is not ready to run sessions yet. Check Settings → About for version and runtime status.';
383
+ }
384
+
385
+ /**
386
+ * Structured 409 body shared by proxy + serverOpenCodeFetch.
387
+ * @param {object} contract
388
+ */
389
+ export function runtimeContractExecutionBlockedBody(contract) {
390
+ return {
391
+ error: formatRuntimeContractBlockedError(contract),
392
+ errorCode: 'RUNTIME_CONTRACT_EXECUTION_BLOCKED',
393
+ phase: contract?.phase ?? null,
394
+ reasons: Array.isArray(contract?.reasons) ? contract.reasons : [],
395
+ serveVersion: contract?.serveVersion ?? null,
396
+ minVerifiedVersion: contract?.minVerifiedVersion ?? null,
397
+ maxVerifiedVersion: contract?.maxVerifiedVersion ?? null,
398
+ pinnedVersion: contract?.pinnedVersion ?? PINNED_OPENCODE2_VERSION,
399
+ contract: contract ?? null,
400
+ };
401
+ }
@@ -0,0 +1,214 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ RUNTIME_CONTRACT_MIN_VERIFIED,
4
+ classifyRuntimeVersionBand,
5
+ createRevokedRuntimeContract,
6
+ evaluateRuntimeContract,
7
+ formatRuntimeContractBlockedError,
8
+ isRuntimeContractDiagnosticPath,
9
+ isRuntimeContractExecutionPath,
10
+ runtimeContractExecutionBlockedBody,
11
+ shouldBlockRuntimeContractExecution,
12
+ versionMeetsMinimum,
13
+ } from './runtime-contract.js';
14
+
15
+ describe('runtime contract admission (ticket 11)', () => {
16
+ it('records minimum verified bound from pin', () => {
17
+ expect(RUNTIME_CONTRACT_MIN_VERIFIED).toBe('2.0.12');
18
+ });
19
+
20
+ it('classifies version bands with a 2.x floor only', () => {
21
+ expect(classifyRuntimeVersionBand('2.0.12')).toBe('verified');
22
+ expect(classifyRuntimeVersionBand('2.0.14')).toBe('verified');
23
+ expect(classifyRuntimeVersionBand('2.0.15')).toBe('verified');
24
+ expect(classifyRuntimeVersionBand('2.1.0')).toBe('verified');
25
+ expect(classifyRuntimeVersionBand('2.0.5')).toBe('below-min');
26
+ expect(classifyRuntimeVersionBand('1.18.18')).toBe('1x');
27
+ expect(classifyRuntimeVersionBand('not-a-version')).toBe('invalid');
28
+ expect(classifyRuntimeVersionBand(null)).toBe('unknown');
29
+ });
30
+
31
+ it('admits verified serve for core protocol and optional caps', () => {
32
+ const result = evaluateRuntimeContract({
33
+ serveVersion: '2.0.12',
34
+ cliVersion: '2.0.12',
35
+ reachable: true,
36
+ authenticated: true,
37
+ healthOk: true,
38
+ migrationAdmitTranscript: true,
39
+ });
40
+ expect(result.phase).toBe('ready');
41
+ expect(result.protocolCompatible).toBe(true);
42
+ expect(result.executionAllowed).toBe(true);
43
+ expect(result.capabilities['core.protocol'].available).toBe(true);
44
+ expect(result.capabilities['session.queuedInput'].available).toBe(true);
45
+ expect(result.capabilities['session.background'].available).toBe(true);
46
+ expect(result.capabilities['session.generationFallback'].available).toBe(true);
47
+ expect(result.reasons).toEqual([]);
48
+ });
49
+
50
+ it('limits older 2.x and keeps diagnostic dimensions explicit', () => {
51
+ const result = evaluateRuntimeContract({
52
+ serveVersion: '2.0.5',
53
+ cliVersion: '2.0.5',
54
+ reachable: true,
55
+ authenticated: true,
56
+ healthOk: true,
57
+ migrationAdmitTranscript: true,
58
+ });
59
+ expect(result.phase).toBe('incompatible');
60
+ expect(result.protocolCompatible).toBe(false);
61
+ expect(result.executionAllowed).toBe(false);
62
+ expect(result.reasons).toContain('below-min-verified');
63
+ expect(result.capabilities['core.protocol'].available).toBe(false);
64
+ });
65
+
66
+ it('admits newer 2.x above the documented floor for execution', () => {
67
+ const result = evaluateRuntimeContract({
68
+ serveVersion: '2.0.15',
69
+ reachable: true,
70
+ authenticated: true,
71
+ healthOk: true,
72
+ migrationAdmitTranscript: true,
73
+ });
74
+ expect(result.versionBand).toBe('verified');
75
+ expect(result.phase).toBe('ready');
76
+ expect(result.protocolCompatible).toBe(true);
77
+ expect(result.executionAllowed).toBe(true);
78
+ expect(result.capabilities['core.protocol'].available).toBe(true);
79
+ expect(result.maxVerifiedVersion).toBeNull();
80
+ });
81
+
82
+ it('rejects 1.x and unparseable versions for execution', () => {
83
+ const oneX = evaluateRuntimeContract({
84
+ serveVersion: '1.18.18',
85
+ reachable: true,
86
+ authenticated: true,
87
+ healthOk: true,
88
+ });
89
+ expect(oneX.phase).toBe('incompatible');
90
+ expect(oneX.executionAllowed).toBe(false);
91
+ expect(oneX.reasons).toContain('1x-version');
92
+
93
+ const bad = evaluateRuntimeContract({
94
+ serveVersion: 'totally-bogus',
95
+ reachable: true,
96
+ authenticated: true,
97
+ healthOk: true,
98
+ });
99
+ expect(bad.phase).toBe('unknown');
100
+ expect(bad.executionAllowed).toBe(false);
101
+ });
102
+
103
+ it('separates auth failure, unreachable, migration block, and cli/serve mismatch', () => {
104
+ expect(evaluateRuntimeContract({ reachable: false }).phase).toBe('unreachable');
105
+ expect(evaluateRuntimeContract({
106
+ reachable: true,
107
+ authenticated: false,
108
+ serveVersion: '2.0.12',
109
+ healthOk: true,
110
+ }).phase).toBe('unauthenticated');
111
+
112
+ const migration = evaluateRuntimeContract({
113
+ serveVersion: '2.0.12',
114
+ reachable: true,
115
+ authenticated: true,
116
+ healthOk: true,
117
+ migrationAdmitTranscript: false,
118
+ migrationPhase: 'running',
119
+ });
120
+ expect(migration.phase).toBe('migration-blocked');
121
+ expect(migration.executionAllowed).toBe(false);
122
+ expect(migration.migrationExecutable).toBe(false);
123
+
124
+ const mismatch = evaluateRuntimeContract({
125
+ serveVersion: '2.0.12',
126
+ cliVersion: '2.0.14',
127
+ reachable: true,
128
+ authenticated: true,
129
+ healthOk: true,
130
+ migrationAdmitTranscript: true,
131
+ });
132
+ expect(mismatch.versionMismatch).toBe(true);
133
+ expect(mismatch.reasons).toContain('cli-serve-version-mismatch');
134
+ // Mismatch is diagnostic; admission still follows serve version.
135
+ expect(mismatch.executionAllowed).toBe(true);
136
+ });
137
+
138
+ it('classifies diagnostic vs execution proxy paths', () => {
139
+ expect(isRuntimeContractDiagnosticPath('GET', '/api/info')).toBe(true);
140
+ expect(isRuntimeContractDiagnosticPath('GET', '/api/health')).toBe(true);
141
+ expect(isRuntimeContractDiagnosticPath('POST', '/api/info')).toBe(false);
142
+ expect(isRuntimeContractExecutionPath('POST', '/api/session/ses_1/prompt')).toBe(true);
143
+ expect(isRuntimeContractExecutionPath('POST', '/api/session/ses_1/prompt_async')).toBe(true);
144
+ expect(isRuntimeContractExecutionPath('GET', '/api/session/ses_1/message')).toBe(false);
145
+ // Stop-task remains available when execution is limited.
146
+ expect(isRuntimeContractExecutionPath('POST', '/api/session/ses_1/interrupt')).toBe(false);
147
+ expect(isRuntimeContractExecutionPath('POST', '/api/session/ses_1/abort')).toBe(false);
148
+ expect(versionMeetsMinimum('2.0.14', '2.0.12')).toBe(true);
149
+ expect(versionMeetsMinimum('2.0.1', '2.0.12')).toBe(false);
150
+ });
151
+
152
+ it.each([
153
+ '/api/session/ses_1/generate',
154
+ '/api/session/ses_1/background',
155
+ '/api/session/ses_1/inbox/msg_1/steer',
156
+ '/api/session/ses_1/inbox/msg_1/queue',
157
+ '/api/session/ses_1/form/form_1/reply',
158
+ '/api/session/ses_1/model',
159
+ '/api/session/ses_1/agent',
160
+ ])('limits the execution control endpoint %s for an unverified runtime', (path) => {
161
+ expect(shouldBlockRuntimeContractExecution('POST', path, { executionAllowed: false })).toBe(true);
162
+ expect(shouldBlockRuntimeContractExecution('POST', path, { executionAllowed: true })).toBe(false);
163
+ expect(shouldBlockRuntimeContractExecution('GET', path, { executionAllowed: false })).toBe(false);
164
+ });
165
+
166
+ it('keeps stopping and cancelling pending work available under limited execution', () => {
167
+ const contract = { executionAllowed: false };
168
+ expect(shouldBlockRuntimeContractExecution('POST', '/api/session/ses_1/interrupt', contract)).toBe(false);
169
+ expect(shouldBlockRuntimeContractExecution('DELETE', '/api/session/ses_1/inbox/msg_1', contract)).toBe(false);
170
+ expect(shouldBlockRuntimeContractExecution('PATCH', '/api/session/ses_1/inbox/msg_1', contract)).toBe(true);
171
+ });
172
+
173
+ it('exposes upgrade guidance in blocked execution responses for below-min serve', () => {
174
+ const contract = evaluateRuntimeContract({
175
+ serveVersion: '2.0.5',
176
+ reachable: true,
177
+ authenticated: true,
178
+ healthOk: true,
179
+ migrationAdmitTranscript: true,
180
+ });
181
+ const body = runtimeContractExecutionBlockedBody(contract);
182
+ expect(body.error).toContain('2.0.12');
183
+ expect(body.error).toMatch(/Settings → About/i);
184
+ expect(formatRuntimeContractBlockedError(contract)).toBe(body.error);
185
+ });
186
+
187
+ it('production default blocks execution when contract is null (no silent bypass)', () => {
188
+ expect(shouldBlockRuntimeContractExecution('POST', '/api/session/ses_1/prompt', null)).toBe(true);
189
+ expect(shouldBlockRuntimeContractExecution('POST', '/api/session/ses_1/prompt', undefined)).toBe(true);
190
+ expect(shouldBlockRuntimeContractExecution('GET', '/api/session/ses_1/message', null)).toBe(false);
191
+ expect(shouldBlockRuntimeContractExecution('POST', '/api/session/ses_1/interrupt', null)).toBe(false);
192
+ });
193
+
194
+ it('requires explicit executionAllowed === true (pending / undefined still block)', () => {
195
+ expect(shouldBlockRuntimeContractExecution('POST', '/api/session/ses_1/prompt', { executionAllowed: true })).toBe(false);
196
+ expect(shouldBlockRuntimeContractExecution('POST', '/api/session/ses_1/prompt', {})).toBe(true);
197
+ expect(shouldBlockRuntimeContractExecution('POST', '/api/session/ses_1/prompt', { executionAllowed: false })).toBe(true);
198
+ const revoked = createRevokedRuntimeContract('instance-revoked', { instanceGeneration: 3 });
199
+ expect(revoked.executionAllowed).toBe(false);
200
+ expect(revoked.phase).toBe('pending');
201
+ expect(revoked.instanceGeneration).toBe(3);
202
+ expect(shouldBlockRuntimeContractExecution('POST', '/api/session/ses_1/prompt', revoked)).toBe(true);
203
+ });
204
+
205
+ it('DI factory may allow missing contract only when explicitly opted in', () => {
206
+ expect(shouldBlockRuntimeContractExecution('POST', '/api/session/ses_1/prompt', null, {
207
+ allowMissingContract: true,
208
+ })).toBe(false);
209
+ // Opt-in does not open denied contracts.
210
+ expect(shouldBlockRuntimeContractExecution('POST', '/api/session/ses_1/prompt', {
211
+ executionAllowed: false,
212
+ }, { allowMissingContract: true })).toBe(true);
213
+ });
214
+ });