@readymadeit/mcp 0.0.0-stage → 0.1.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 (916) hide show
  1. package/README.md +114 -2
  2. package/bundle/LICENSE +201 -0
  3. package/bundle/boards/aggregator-demo-board.json +54 -0
  4. package/bundle/boards/alpaca-board.json +316 -0
  5. package/bundle/boards/animate-board.json +159 -0
  6. package/bundle/boards/ascii-art-demo-board.json +33 -0
  7. package/bundle/boards/ascii-cam-board.json +184 -0
  8. package/bundle/boards/asset-demo-board.json +54 -0
  9. package/bundle/boards/audio-encode-demo-board.json +153 -0
  10. package/bundle/boards/audiolize-board.json +152 -0
  11. package/bundle/boards/batcher-demo-board.json +43 -0
  12. package/bundle/boards/buffer-demo-board.json +53 -0
  13. package/bundle/boards/cache-demo-board.json +59 -0
  14. package/bundle/boards/camera-demo-board.json +28 -0
  15. package/bundle/boards/canvas-demo-board.json +55 -0
  16. package/bundle/boards/changes-demo-board.json +60 -0
  17. package/bundle/boards/court-booking-browser-demo-board.json +221 -0
  18. package/bundle/boards/court-booking-demo-board.json +218 -0
  19. package/bundle/boards/current-time-board.json +32 -0
  20. package/bundle/boards/debounce-demo-board.json +43 -0
  21. package/bundle/boards/decrypt-demo-board.json +47 -0
  22. package/bundle/boards/delay-demo-board.json +28 -0
  23. package/bundle/boards/detect-demo-board.json +64 -0
  24. package/bundle/boards/document-extract-demo-board.json +55 -0
  25. package/bundle/boards/dropitapp-board.json +138 -0
  26. package/bundle/boards/encode-demo-board.json +39 -0
  27. package/bundle/boards/encrypt-board.json +185 -0
  28. package/bundle/boards/encrypt-demo-board.json +39 -0
  29. package/bundle/boards/face-alert-board.json +181 -0
  30. package/bundle/boards/fetcher-demo-board.json +40 -0
  31. package/bundle/boards/fft-demo-board.json +28 -0
  32. package/bundle/boards/filter-demo-board.json +55 -0
  33. package/bundle/boards/flat-map-demo-board.json +42 -0
  34. package/bundle/boards/frame-limit-test-board.json +130 -0
  35. package/bundle/boards/game-board.json +413 -0
  36. package/bundle/boards/game-of-life-board.json +139 -0
  37. package/bundle/boards/game-of-life-demo-board.json +34 -0
  38. package/bundle/boards/gather-demo-board.json +62 -0
  39. package/bundle/boards/group-by-demo-board.json +62 -0
  40. package/bundle/boards/hacker-demo-board.json +45 -0
  41. package/bundle/boards/hacker-news-board.json +741 -0
  42. package/bundle/boards/hash-demo-board.json +40 -0
  43. package/bundle/boards/health-log-browser-demo-board.json +537 -0
  44. package/bundle/boards/health-log-demo-board.json +498 -0
  45. package/bundle/boards/helloworld-board.json +57 -0
  46. package/bundle/boards/hold-demo-board.json +67 -0
  47. package/bundle/boards/hold-scopes-demo-board.json +141 -0
  48. package/bundle/boards/http-client-browser-demo-board.json +379 -0
  49. package/bundle/boards/http-client-demo-board.json +380 -0
  50. package/bundle/boards/image-picker-demo-board.json +22 -0
  51. package/bundle/boards/imap-email-demo-board.json +35 -0
  52. package/bundle/boards/injector-demo-board.json +55 -0
  53. package/bundle/boards/iterator-demo-board.json +55 -0
  54. package/bundle/boards/limit-demo-board.json +66 -0
  55. package/bundle/boards/link-debugger.json +32 -0
  56. package/bundle/boards/live-location-demo-board.json +93 -0
  57. package/bundle/boards/live-radio-cloud-demo-board.json +216 -0
  58. package/bundle/boards/live-radio-cloud-direct-demo-board.json +311 -0
  59. package/bundle/boards/live-radio-demo-board.json +211 -0
  60. package/bundle/boards/live-radio-members-demo-board.json +314 -0
  61. package/bundle/boards/local-storage-demo-board.json +30 -0
  62. package/bundle/boards/lz-compress-demo-board.json +52 -0
  63. package/bundle/boards/map-demo-board.json +46 -0
  64. package/bundle/boards/match-filter-demo-board.json +43 -0
  65. package/bundle/boards/meeting-poll-browser-demo-board.json +649 -0
  66. package/bundle/boards/meeting-poll-demo-board.json +650 -0
  67. package/bundle/boards/microphone-monitor-demo-board.json +28 -0
  68. package/bundle/boards/microphone-speaker-demo-board.json +86 -0
  69. package/bundle/boards/monitor-demo-board.json +52 -0
  70. package/bundle/boards/mounted-endpoint-demo-board-hkp-rt.json +84 -0
  71. package/bundle/boards/mounted-endpoint-demo-board.json +84 -0
  72. package/bundle/boards/nested-rhythm-demo-board.json +1160 -0
  73. package/bundle/boards/next-bus-board.json +52 -0
  74. package/bundle/boards/next-bus-switch-board.json +79 -0
  75. package/bundle/boards/night-groove-demo-board.json +742 -0
  76. package/bundle/boards/noise-alert-board.json +146 -0
  77. package/bundle/boards/offer-followup-demo-board.json +291 -0
  78. package/bundle/boards/offer-intake-demo-board.json +289 -0
  79. package/bundle/boards/offer-intake-hetzner-demo-board.json +286 -0
  80. package/bundle/boards/offer-intake-imap-demo-board.json +289 -0
  81. package/bundle/boards/offer-intake-imap-hetzner-demo-board.json +354 -0
  82. package/bundle/boards/p2p-receiver-demo-board.json +65 -0
  83. package/bundle/boards/p2p-sender-demo-board.json +94 -0
  84. package/bundle/boards/pad-groove-demo-board.json +644 -0
  85. package/bundle/boards/peer-chat-board.json +109 -0
  86. package/bundle/boards/peer-chat-node-board.json +97 -0
  87. package/bundle/boards/qr-code-demo-board.json +22 -0
  88. package/bundle/boards/queue-demo-board.json +239 -0
  89. package/bundle/boards/reduce-demo-board.json +54 -0
  90. package/bundle/boards/remote-named-demo-board-hkp-rt.json +61 -0
  91. package/bundle/boards/remote-named-demo-board.json +61 -0
  92. package/bundle/boards/rss-demo-board.json +530 -0
  93. package/bundle/boards/rss-radio-board.json +515 -0
  94. package/bundle/boards/rss-radio-brief-one-board.json +237 -0
  95. package/bundle/boards/rss-radio-editor-unit-board.json +555 -0
  96. package/bundle/boards/rss-radio-library-unit-board.json +293 -0
  97. package/bundle/boards/rss-radio-unit-board.json +809 -0
  98. package/bundle/boards/rss-radio-voice-unit-board.json +146 -0
  99. package/bundle/boards/runtime-configs/hold-example.json +41 -0
  100. package/bundle/boards/runtime-configs/injector-example.json +23 -0
  101. package/bundle/boards/runtime-configs/speech-to-text-example.json +35 -0
  102. package/bundle/boards/runtime-configs/text-generation-example.json +31 -0
  103. package/bundle/boards/runtime-configs/text-to-speech-example.json +28 -0
  104. package/bundle/boards/runtime-configs/text-to-speech-inflect-example.json +28 -0
  105. package/bundle/boards/runtime-configs/ws-audio-playback-example.json +23 -0
  106. package/bundle/boards/select-demo-board.json +32 -0
  107. package/bundle/boards/send-ntfy-board.json +52 -0
  108. package/bundle/boards/shared-costs-demo-board.json +361 -0
  109. package/bundle/boards/sign-demo-board.json +41 -0
  110. package/bundle/boards/skill-router-demo-board.json +210 -0
  111. package/bundle/boards/smooth-demo-board.json +91 -0
  112. package/bundle/boards/smtp-email-demo-board.json +34 -0
  113. package/bundle/boards/sort-demo-board.json +55 -0
  114. package/bundle/boards/sound-demo-board.json +65 -0
  115. package/bundle/boards/spectral-modifier-board.json +98 -0
  116. package/bundle/boards/speech-synth-demo-board.json +37 -0
  117. package/bundle/boards/speech-to-text-demo-board.json +146 -0
  118. package/bundle/boards/spotify-board.json +75 -0
  119. package/bundle/boards/spread-demo-board.json +35 -0
  120. package/bundle/boards/sql-demo-board.json +56 -0
  121. package/bundle/boards/sql-explorer-board.json +447 -0
  122. package/bundle/boards/sql-explorer-node-board.json +474 -0
  123. package/bundle/boards/sql-import-board.json +156 -0
  124. package/bundle/boards/state-demo-board.json +65 -0
  125. package/bundle/boards/stopper-demo-board.json +40 -0
  126. package/bundle/boards/storage-demo-board.json +193 -0
  127. package/bundle/boards/store-demo-board.json +63 -0
  128. package/bundle/boards/swimlanes-demo-board.json +197 -0
  129. package/bundle/boards/switch-demo-board.json +51 -0
  130. package/bundle/boards/syn-board.json +19 -0
  131. package/bundle/boards/syn-booking-unit-board.json +998 -0
  132. package/bundle/boards/syn-conversations-anthropic-demo-board.json +925 -0
  133. package/bundle/boards/syn-conversations-demo-board.json +925 -0
  134. package/bundle/boards/syn-hotels-unit-board.json +287 -0
  135. package/bundle/boards/syn-mail-desk-board.json +25 -0
  136. package/bundle/boards/syn-mail-desk-unit-board.json +254 -0
  137. package/bundle/boards/telegram-listener-demo-board.json +31 -0
  138. package/bundle/boards/text-generation-anthropic-demo-board.json +66 -0
  139. package/bundle/boards/text-generation-demo-board.json +51 -0
  140. package/bundle/boards/text-generation-local-demo-board.json +80 -0
  141. package/bundle/boards/text-to-speech-demo-board.json +58 -0
  142. package/bundle/boards/timeline-demo-board.json +477 -0
  143. package/bundle/boards/timer-demo-board.json +35 -0
  144. package/bundle/boards/tracks-demo-board.json +95 -0
  145. package/bundle/boards/trigger-pad-demo-board.json +47 -0
  146. package/bundle/boards/triggerpad-board.json +31 -0
  147. package/bundle/boards/triggerpad-recorder-board.json +56 -0
  148. package/bundle/boards/uuid-generator-demo-board.json +166 -0
  149. package/bundle/boards/voice-assistant-demo-board.json +230 -0
  150. package/bundle/boards/voice-assistant-hkp-rt-demo-board.json +239 -0
  151. package/bundle/boards/voice-assistant-local-demo-board.json +232 -0
  152. package/bundle/boards/voice-assistant-skills-demo-board.json +316 -0
  153. package/bundle/boards/voice-assistant-skills-local-demo-board.json +316 -0
  154. package/bundle/boards/xy-pad-demo-board.json +36 -0
  155. package/bundle/docs/content/board-json.md +277 -0
  156. package/bundle/docs/content/boards/alpaca-board.md +56 -0
  157. package/bundle/docs/content/boards/animate-board.md +40 -0
  158. package/bundle/docs/content/boards/ascii-cam-board.md +37 -0
  159. package/bundle/docs/content/boards/audiolize-board.md +47 -0
  160. package/bundle/docs/content/boards/court-booking-browser-demo-board.md +43 -0
  161. package/bundle/docs/content/boards/court-booking-demo-board.md +120 -0
  162. package/bundle/docs/content/boards/current-time-board.md +32 -0
  163. package/bundle/docs/content/boards/dropitapp-board.md +57 -0
  164. package/bundle/docs/content/boards/encrypt-board.md +36 -0
  165. package/bundle/docs/content/boards/face-alert-board.md +50 -0
  166. package/bundle/docs/content/boards/game-board.md +69 -0
  167. package/bundle/docs/content/boards/game-of-life-board.md +47 -0
  168. package/bundle/docs/content/boards/hacker-news-board.md +76 -0
  169. package/bundle/docs/content/boards/health-log-browser-demo-board.md +56 -0
  170. package/bundle/docs/content/boards/health-log-demo-board.md +138 -0
  171. package/bundle/docs/content/boards/helloworld-board.md +40 -0
  172. package/bundle/docs/content/boards/link-debugger.md +31 -0
  173. package/bundle/docs/content/boards/live-radio-cloud-demo-board.md +70 -0
  174. package/bundle/docs/content/boards/live-radio-cloud-direct-demo-board.md +89 -0
  175. package/bundle/docs/content/boards/live-radio-demo-board.md +102 -0
  176. package/bundle/docs/content/boards/live-radio-members-demo-board.md +91 -0
  177. package/bundle/docs/content/boards/meeting-poll-browser-demo-board.md +45 -0
  178. package/bundle/docs/content/boards/meeting-poll-demo-board.md +121 -0
  179. package/bundle/docs/content/boards/mounted-endpoint-demo-board-hkp-rt.md +50 -0
  180. package/bundle/docs/content/boards/nested-rhythm-demo-board.md +165 -0
  181. package/bundle/docs/content/boards/next-bus-board.md +38 -0
  182. package/bundle/docs/content/boards/next-bus-switch-board.md +44 -0
  183. package/bundle/docs/content/boards/night-groove-demo-board.md +110 -0
  184. package/bundle/docs/content/boards/noise-alert-board.md +44 -0
  185. package/bundle/docs/content/boards/pad-groove-demo-board.md +93 -0
  186. package/bundle/docs/content/boards/peer-chat-board.md +49 -0
  187. package/bundle/docs/content/boards/peer-chat-node-board.md +47 -0
  188. package/bundle/docs/content/boards/rss-radio-board.md +221 -0
  189. package/bundle/docs/content/boards/send-ntfy-board.md +33 -0
  190. package/bundle/docs/content/boards/shared-costs-demo-board.md +65 -0
  191. package/bundle/docs/content/boards/spectral-modifier-board.md +55 -0
  192. package/bundle/docs/content/boards/spotify-board.md +46 -0
  193. package/bundle/docs/content/boards/sql-explorer-board.md +70 -0
  194. package/bundle/docs/content/boards/sql-explorer-node-board.md +76 -0
  195. package/bundle/docs/content/boards/sql-import-board.md +37 -0
  196. package/bundle/docs/content/boards/swimlanes-demo-board.md +148 -0
  197. package/bundle/docs/content/boards/syn-mail-desk-board.md +52 -0
  198. package/bundle/docs/content/boards/timeline-demo-board.md +115 -0
  199. package/bundle/docs/content/boards/triggerpad-board.md +32 -0
  200. package/bundle/docs/content/boards/triggerpad-recorder-board.md +52 -0
  201. package/bundle/docs/content/boards/voice-assistant-demo-board.md +51 -0
  202. package/bundle/docs/content/concepts/assets.md +245 -0
  203. package/bundle/docs/content/concepts/blocks.md +300 -0
  204. package/bundle/docs/content/concepts/board-assistant.md +307 -0
  205. package/bundle/docs/content/concepts/board.md +265 -0
  206. package/bundle/docs/content/concepts/cloud-boards.md +803 -0
  207. package/bundle/docs/content/concepts/coordinator.md +297 -0
  208. package/bundle/docs/content/concepts/logging.md +293 -0
  209. package/bundle/docs/content/concepts/mounts.md +314 -0
  210. package/bundle/docs/content/concepts/presets.md +360 -0
  211. package/bundle/docs/content/concepts/remotes.md +423 -0
  212. package/bundle/docs/content/concepts/runtime.md +664 -0
  213. package/bundle/docs/content/concepts/scopes.md +256 -0
  214. package/bundle/docs/content/concepts/service.md +227 -0
  215. package/bundle/docs/content/concepts/units.md +321 -0
  216. package/bundle/docs/content/introduction.md +163 -0
  217. package/bundle/docs/content/llm/circle-text-board.json +55 -0
  218. package/bundle/docs/content/llm/howto-workflow.md +369 -0
  219. package/bundle/docs/content/repository.md +161 -0
  220. package/bundle/docs/content/services/aggregator.md +76 -0
  221. package/bundle/docs/content/services/analyzer.md +44 -0
  222. package/bundle/docs/content/services/ascii-art.md +58 -0
  223. package/bundle/docs/content/services/asset.md +88 -0
  224. package/bundle/docs/content/services/audio-editor.md +57 -0
  225. package/bundle/docs/content/services/audio-encode.md +115 -0
  226. package/bundle/docs/content/services/audio-io.md +173 -0
  227. package/bundle/docs/content/services/batcher.md +49 -0
  228. package/bundle/docs/content/services/board-service.md +48 -0
  229. package/bundle/docs/content/services/browser-sub-service.md +75 -0
  230. package/bundle/docs/content/services/buffer.md +70 -0
  231. package/bundle/docs/content/services/cache.md +92 -0
  232. package/bundle/docs/content/services/camera.md +85 -0
  233. package/bundle/docs/content/services/canvas.md +196 -0
  234. package/bundle/docs/content/services/changes.md +87 -0
  235. package/bundle/docs/content/services/chunked-file-provider.md +63 -0
  236. package/bundle/docs/content/services/communication-dispatcher.md +190 -0
  237. package/bundle/docs/content/services/configurator.md +66 -0
  238. package/bundle/docs/content/services/conversations.md +244 -0
  239. package/bundle/docs/content/services/debounce.md +45 -0
  240. package/bundle/docs/content/services/decrypt.md +53 -0
  241. package/bundle/docs/content/services/delay.md +55 -0
  242. package/bundle/docs/content/services/detect.md +101 -0
  243. package/bundle/docs/content/services/document-extract.md +174 -0
  244. package/bundle/docs/content/services/download.md +67 -0
  245. package/bundle/docs/content/services/encode.md +63 -0
  246. package/bundle/docs/content/services/encrypt.md +53 -0
  247. package/bundle/docs/content/services/fetcher.md +122 -0
  248. package/bundle/docs/content/services/fft.md +96 -0
  249. package/bundle/docs/content/services/file-source.md +67 -0
  250. package/bundle/docs/content/services/filesystem.md +157 -0
  251. package/bundle/docs/content/services/filter.md +133 -0
  252. package/bundle/docs/content/services/flat-map.md +62 -0
  253. package/bundle/docs/content/services/game-of-life.md +93 -0
  254. package/bundle/docs/content/services/gather.md +135 -0
  255. package/bundle/docs/content/services/gif-encoder.md +56 -0
  256. package/bundle/docs/content/services/github.md +85 -0
  257. package/bundle/docs/content/services/group-by.md +54 -0
  258. package/bundle/docs/content/services/hacker.md +79 -0
  259. package/bundle/docs/content/services/hash.md +59 -0
  260. package/bundle/docs/content/services/hold.md +182 -0
  261. package/bundle/docs/content/services/http-relay-client.md +48 -0
  262. package/bundle/docs/content/services/http-uploader.md +62 -0
  263. package/bundle/docs/content/services/http.md +424 -0
  264. package/bundle/docs/content/services/if.md +79 -0
  265. package/bundle/docs/content/services/ifft.md +46 -0
  266. package/bundle/docs/content/services/image-picker.md +50 -0
  267. package/bundle/docs/content/services/imap-email.md +104 -0
  268. package/bundle/docs/content/services/injector.md +129 -0
  269. package/bundle/docs/content/services/input.md +90 -0
  270. package/bundle/docs/content/services/iterator.md +186 -0
  271. package/bundle/docs/content/services/join.md +153 -0
  272. package/bundle/docs/content/services/limit.md +47 -0
  273. package/bundle/docs/content/services/local-storage.md +47 -0
  274. package/bundle/docs/content/services/looper.md +62 -0
  275. package/bundle/docs/content/services/lz-compress.md +52 -0
  276. package/bundle/docs/content/services/map.md +257 -0
  277. package/bundle/docs/content/services/match-filter.md +38 -0
  278. package/bundle/docs/content/services/microphone-monitor.md +66 -0
  279. package/bundle/docs/content/services/monitor.md +67 -0
  280. package/bundle/docs/content/services/ollama.md +116 -0
  281. package/bundle/docs/content/services/openai-prompt.md +74 -0
  282. package/bundle/docs/content/services/output.md +107 -0
  283. package/bundle/docs/content/services/peer-socket.md +61 -0
  284. package/bundle/docs/content/services/qr-code.md +48 -0
  285. package/bundle/docs/content/services/queue.md +201 -0
  286. package/bundle/docs/content/services/reduce.md +51 -0
  287. package/bundle/docs/content/services/rss.md +232 -0
  288. package/bundle/docs/content/services/select.md +47 -0
  289. package/bundle/docs/content/services/sign.md +53 -0
  290. package/bundle/docs/content/services/skill-router.md +154 -0
  291. package/bundle/docs/content/services/smooth.md +59 -0
  292. package/bundle/docs/content/services/smtp-email.md +129 -0
  293. package/bundle/docs/content/services/sort.md +48 -0
  294. package/bundle/docs/content/services/sound.md +87 -0
  295. package/bundle/docs/content/services/speech-synth.md +46 -0
  296. package/bundle/docs/content/services/speech-to-text.md +172 -0
  297. package/bundle/docs/content/services/spotify.md +77 -0
  298. package/bundle/docs/content/services/spread.md +111 -0
  299. package/bundle/docs/content/services/sql.md +491 -0
  300. package/bundle/docs/content/services/state.md +57 -0
  301. package/bundle/docs/content/services/static.md +47 -0
  302. package/bundle/docs/content/services/stopper.md +55 -0
  303. package/bundle/docs/content/services/storage.md +155 -0
  304. package/bundle/docs/content/services/store.md +255 -0
  305. package/bundle/docs/content/services/sub-service.md +152 -0
  306. package/bundle/docs/content/services/switch.md +67 -0
  307. package/bundle/docs/content/services/telegram-listener.md +76 -0
  308. package/bundle/docs/content/services/text-generation.md +390 -0
  309. package/bundle/docs/content/services/text-to-speech.md +130 -0
  310. package/bundle/docs/content/services/timeline.md +333 -0
  311. package/bundle/docs/content/services/timer.md +113 -0
  312. package/bundle/docs/content/services/tracks.md +207 -0
  313. package/bundle/docs/content/services/trigger-pad.md +68 -0
  314. package/bundle/docs/content/services/uuid-generator.md +48 -0
  315. package/bundle/docs/content/services/wav-reader.md +66 -0
  316. package/bundle/docs/content/services/websocket.md +189 -0
  317. package/bundle/docs/content/services/workflow-board-builder.md +65 -0
  318. package/bundle/docs/content/services/xy-pad.md +60 -0
  319. package/bundle/docs/content/targets.md +434 -0
  320. package/bundle/docs/content/testing.md +263 -0
  321. package/bundle/docs/content/vocabulary.md +171 -0
  322. package/bundle/hkp-frontend/src/runtime/browser/BrowserRegistry.tsx +141 -0
  323. package/bundle/hkp-frontend/src/runtime/browser/BrowserRuntime.tsx +202 -0
  324. package/bundle/hkp-frontend/src/runtime/browser/BrowserRuntimeApi.ts +306 -0
  325. package/bundle/hkp-frontend/src/runtime/browser/BrowserRuntimeApp.ts +310 -0
  326. package/bundle/hkp-frontend/src/runtime/browser/BrowserRuntimeScope.ts +500 -0
  327. package/bundle/hkp-frontend/src/runtime/browser/UIRegistry.ts +150 -0
  328. package/bundle/hkp-frontend/src/runtime/browser/assetResolver.ts +123 -0
  329. package/bundle/hkp-frontend/src/runtime/browser/pipelineNames.ts +65 -0
  330. package/bundle/hkp-frontend/src/runtime/browser/registry/Default.ts +237 -0
  331. package/bundle/hkp-frontend/src/runtime/browser/services/Aggregator.ts +127 -0
  332. package/bundle/hkp-frontend/src/runtime/browser/services/AggregatorUI.tsx +83 -0
  333. package/bundle/hkp-frontend/src/runtime/browser/services/Analyzer.tsx +43 -0
  334. package/bundle/hkp-frontend/src/runtime/browser/services/AnalyzerUI/Bars.tsx +33 -0
  335. package/bundle/hkp-frontend/src/runtime/browser/services/AnalyzerUI/HorizontalSpectrogram.tsx +53 -0
  336. package/bundle/hkp-frontend/src/runtime/browser/services/AnalyzerUI/VerticalSpectogram.tsx +56 -0
  337. package/bundle/hkp-frontend/src/runtime/browser/services/AnalyzerUI/Waveform.tsx +109 -0
  338. package/bundle/hkp-frontend/src/runtime/browser/services/AnalyzerUI/index.tsx +99 -0
  339. package/bundle/hkp-frontend/src/runtime/browser/services/AsciiArt.ts +119 -0
  340. package/bundle/hkp-frontend/src/runtime/browser/services/AsciiArtUI.tsx +131 -0
  341. package/bundle/hkp-frontend/src/runtime/browser/services/Asset.ts +125 -0
  342. package/bundle/hkp-frontend/src/runtime/browser/services/AssetUI.tsx +37 -0
  343. package/bundle/hkp-frontend/src/runtime/browser/services/AudioDeviceSettings.tsx +56 -0
  344. package/bundle/hkp-frontend/src/runtime/browser/services/AudioEditor.ts +129 -0
  345. package/bundle/hkp-frontend/src/runtime/browser/services/AudioEditorUI.tsx +549 -0
  346. package/bundle/hkp-frontend/src/runtime/browser/services/AudioInput.ts +298 -0
  347. package/bundle/hkp-frontend/src/runtime/browser/services/AudioInputUI.tsx +257 -0
  348. package/bundle/hkp-frontend/src/runtime/browser/services/AudioOutput.ts +189 -0
  349. package/bundle/hkp-frontend/src/runtime/browser/services/AudioOutputUI.tsx +63 -0
  350. package/bundle/hkp-frontend/src/runtime/browser/services/Batcher.ts +11 -0
  351. package/bundle/hkp-frontend/src/runtime/browser/services/BoardService.tsx +86 -0
  352. package/bundle/hkp-frontend/src/runtime/browser/services/BoardServiceUI.tsx +410 -0
  353. package/bundle/hkp-frontend/src/runtime/browser/services/BrowserGather.ts +311 -0
  354. package/bundle/hkp-frontend/src/runtime/browser/services/BrowserIterator.ts +167 -0
  355. package/bundle/hkp-frontend/src/runtime/browser/services/BrowserSubService.tsx +583 -0
  356. package/bundle/hkp-frontend/src/runtime/browser/services/BrowserSubServiceUI.tsx +187 -0
  357. package/bundle/hkp-frontend/src/runtime/browser/services/BrowserTracks.tsx +477 -0
  358. package/bundle/hkp-frontend/src/runtime/browser/services/Buffer.ts +11 -0
  359. package/bundle/hkp-frontend/src/runtime/browser/services/Cache.ts +134 -0
  360. package/bundle/hkp-frontend/src/runtime/browser/services/CacheUI.tsx +125 -0
  361. package/bundle/hkp-frontend/src/runtime/browser/services/Camera.ts +90 -0
  362. package/bundle/hkp-frontend/src/runtime/browser/services/CameraUI.tsx +321 -0
  363. package/bundle/hkp-frontend/src/runtime/browser/services/Canvas.ts +139 -0
  364. package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/canvasDraw.ts +543 -0
  365. package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/canvasUtils.ts +39 -0
  366. package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/index.tsx +287 -0
  367. package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/tests/canvasDraw.test.ts +355 -0
  368. package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/tests/canvasUtils.test.ts +89 -0
  369. package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/tests/fullscreen.test.tsx +26 -0
  370. package/bundle/hkp-frontend/src/runtime/browser/services/Changes.ts +201 -0
  371. package/bundle/hkp-frontend/src/runtime/browser/services/ChangesUI.tsx +85 -0
  372. package/bundle/hkp-frontend/src/runtime/browser/services/ChunkedFileProvider.tsx +183 -0
  373. package/bundle/hkp-frontend/src/runtime/browser/services/Configurator.ts +258 -0
  374. package/bundle/hkp-frontend/src/runtime/browser/services/ConfiguratorUI.tsx +83 -0
  375. package/bundle/hkp-frontend/src/runtime/browser/services/ConsideredHacker.tsx +28 -0
  376. package/bundle/hkp-frontend/src/runtime/browser/services/Debounce.ts +75 -0
  377. package/bundle/hkp-frontend/src/runtime/browser/services/DebounceUI.tsx +70 -0
  378. package/bundle/hkp-frontend/src/runtime/browser/services/Decrypt.ts +127 -0
  379. package/bundle/hkp-frontend/src/runtime/browser/services/DecryptUI.tsx +39 -0
  380. package/bundle/hkp-frontend/src/runtime/browser/services/Delay.tsx +110 -0
  381. package/bundle/hkp-frontend/src/runtime/browser/services/Detect.ts +214 -0
  382. package/bundle/hkp-frontend/src/runtime/browser/services/DetectUI.tsx +86 -0
  383. package/bundle/hkp-frontend/src/runtime/browser/services/Download.ts +120 -0
  384. package/bundle/hkp-frontend/src/runtime/browser/services/DownloadUI.tsx +61 -0
  385. package/bundle/hkp-frontend/src/runtime/browser/services/Encode.ts +138 -0
  386. package/bundle/hkp-frontend/src/runtime/browser/services/EncodeUI.tsx +52 -0
  387. package/bundle/hkp-frontend/src/runtime/browser/services/Encrypt.ts +125 -0
  388. package/bundle/hkp-frontend/src/runtime/browser/services/EncryptUI.tsx +39 -0
  389. package/bundle/hkp-frontend/src/runtime/browser/services/FFT.tsx +238 -0
  390. package/bundle/hkp-frontend/src/runtime/browser/services/FFTUI.tsx +112 -0
  391. package/bundle/hkp-frontend/src/runtime/browser/services/FeedbackService.ts +247 -0
  392. package/bundle/hkp-frontend/src/runtime/browser/services/FeedbackServiceUI.tsx +35 -0
  393. package/bundle/hkp-frontend/src/runtime/browser/services/Fetcher.tsx +232 -0
  394. package/bundle/hkp-frontend/src/runtime/browser/services/FetcherUI.tsx +154 -0
  395. package/bundle/hkp-frontend/src/runtime/browser/services/FileSource.tsx +171 -0
  396. package/bundle/hkp-frontend/src/runtime/browser/services/Filter.tsx +18 -0
  397. package/bundle/hkp-frontend/src/runtime/browser/services/FilterUI.tsx +89 -0
  398. package/bundle/hkp-frontend/src/runtime/browser/services/FlatMap.ts +68 -0
  399. package/bundle/hkp-frontend/src/runtime/browser/services/FlatMapUI.tsx +38 -0
  400. package/bundle/hkp-frontend/src/runtime/browser/services/GameOfLife.ts +210 -0
  401. package/bundle/hkp-frontend/src/runtime/browser/services/GameOfLifeRenderer.ts +182 -0
  402. package/bundle/hkp-frontend/src/runtime/browser/services/GameOfLifeRendererUI.tsx +48 -0
  403. package/bundle/hkp-frontend/src/runtime/browser/services/GameOfLifeUI.tsx +58 -0
  404. package/bundle/hkp-frontend/src/runtime/browser/services/GatherUI.tsx +84 -0
  405. package/bundle/hkp-frontend/src/runtime/browser/services/GifEncoder.tsx +651 -0
  406. package/bundle/hkp-frontend/src/runtime/browser/services/GithubAPI.ts +294 -0
  407. package/bundle/hkp-frontend/src/runtime/browser/services/GithubComponents.tsx +389 -0
  408. package/bundle/hkp-frontend/src/runtime/browser/services/GithubSink.tsx +109 -0
  409. package/bundle/hkp-frontend/src/runtime/browser/services/GithubSinkUI.tsx +143 -0
  410. package/bundle/hkp-frontend/src/runtime/browser/services/GithubSource.tsx +86 -0
  411. package/bundle/hkp-frontend/src/runtime/browser/services/GithubSourceUI.tsx +162 -0
  412. package/bundle/hkp-frontend/src/runtime/browser/services/GroupBy.ts +11 -0
  413. package/bundle/hkp-frontend/src/runtime/browser/services/HTTPUploader.ts +102 -0
  414. package/bundle/hkp-frontend/src/runtime/browser/services/Hacker.ts +73 -0
  415. package/bundle/hkp-frontend/src/runtime/browser/services/HackerUI.tsx +134 -0
  416. package/bundle/hkp-frontend/src/runtime/browser/services/Hash.ts +134 -0
  417. package/bundle/hkp-frontend/src/runtime/browser/services/HashUI.tsx +45 -0
  418. package/bundle/hkp-frontend/src/runtime/browser/services/Hold.ts +288 -0
  419. package/bundle/hkp-frontend/src/runtime/browser/services/HoldUI.tsx +43 -0
  420. package/bundle/hkp-frontend/src/runtime/browser/services/HttpClient.ts +558 -0
  421. package/bundle/hkp-frontend/src/runtime/browser/services/HttpClientUI.tsx +212 -0
  422. package/bundle/hkp-frontend/src/runtime/browser/services/HttpUploaderTokenInjector.ts +149 -0
  423. package/bundle/hkp-frontend/src/runtime/browser/services/IfService.ts +231 -0
  424. package/bundle/hkp-frontend/src/runtime/browser/services/IfServiceUI.tsx +50 -0
  425. package/bundle/hkp-frontend/src/runtime/browser/services/ImagePicker.tsx +118 -0
  426. package/bundle/hkp-frontend/src/runtime/browser/services/Injector.ts +61 -0
  427. package/bundle/hkp-frontend/src/runtime/browser/services/InjectorUI.tsx +389 -0
  428. package/bundle/hkp-frontend/src/runtime/browser/services/Input.ts +214 -0
  429. package/bundle/hkp-frontend/src/runtime/browser/services/InputUI.tsx +71 -0
  430. package/bundle/hkp-frontend/src/runtime/browser/services/IteratorUI.tsx +59 -0
  431. package/bundle/hkp-frontend/src/runtime/browser/services/LZCompress.ts +63 -0
  432. package/bundle/hkp-frontend/src/runtime/browser/services/Limit.ts +39 -0
  433. package/bundle/hkp-frontend/src/runtime/browser/services/LimitUI.tsx +41 -0
  434. package/bundle/hkp-frontend/src/runtime/browser/services/LocalStorage.ts +65 -0
  435. package/bundle/hkp-frontend/src/runtime/browser/services/LocalStorageUI.tsx +72 -0
  436. package/bundle/hkp-frontend/src/runtime/browser/services/Map.ts +14 -0
  437. package/bundle/hkp-frontend/src/runtime/browser/services/MapUI.tsx +334 -0
  438. package/bundle/hkp-frontend/src/runtime/browser/services/MapUIModeAssist.tsx +61 -0
  439. package/bundle/hkp-frontend/src/runtime/browser/services/MatchFilter.ts +11 -0
  440. package/bundle/hkp-frontend/src/runtime/browser/services/MicrophoneMonitor.ts +190 -0
  441. package/bundle/hkp-frontend/src/runtime/browser/services/MicrophoneMonitorUI.tsx +94 -0
  442. package/bundle/hkp-frontend/src/runtime/browser/services/Monitor.tsx +15 -0
  443. package/bundle/hkp-frontend/src/runtime/browser/services/MonitorUI.tsx +281 -0
  444. package/bundle/hkp-frontend/src/runtime/browser/services/OllamaPrompt.ts +237 -0
  445. package/bundle/hkp-frontend/src/runtime/browser/services/OllamaPromptUI/BusyIndicator.tsx +18 -0
  446. package/bundle/hkp-frontend/src/runtime/browser/services/OllamaPromptUI/PromptInput.tsx +71 -0
  447. package/bundle/hkp-frontend/src/runtime/browser/services/OllamaPromptUI/StreamingPreview.tsx +24 -0
  448. package/bundle/hkp-frontend/src/runtime/browser/services/OllamaPromptUI/index.tsx +156 -0
  449. package/bundle/hkp-frontend/src/runtime/browser/services/OpenAIPrompt.ts +254 -0
  450. package/bundle/hkp-frontend/src/runtime/browser/services/OpenAIPromptUI.tsx +189 -0
  451. package/bundle/hkp-frontend/src/runtime/browser/services/Output.tsx +145 -0
  452. package/bundle/hkp-frontend/src/runtime/browser/services/OutputUI.tsx +118 -0
  453. package/bundle/hkp-frontend/src/runtime/browser/services/PeerChatTemplate.json +43 -0
  454. package/bundle/hkp-frontend/src/runtime/browser/services/PeerConnection.ts +260 -0
  455. package/bundle/hkp-frontend/src/runtime/browser/services/PeerSocket.tsx +268 -0
  456. package/bundle/hkp-frontend/src/runtime/browser/services/PeerSocketReceiverTemplate.json +29 -0
  457. package/bundle/hkp-frontend/src/runtime/browser/services/PeerSocketUI.tsx +341 -0
  458. package/bundle/hkp-frontend/src/runtime/browser/services/PlaceholderUI.tsx +72 -0
  459. package/bundle/hkp-frontend/src/runtime/browser/services/QrCode.ts +63 -0
  460. package/bundle/hkp-frontend/src/runtime/browser/services/QrCodeUI.tsx +197 -0
  461. package/bundle/hkp-frontend/src/runtime/browser/services/Select.ts +40 -0
  462. package/bundle/hkp-frontend/src/runtime/browser/services/SelectUI.tsx +41 -0
  463. package/bundle/hkp-frontend/src/runtime/browser/services/ServiceBase.ts +50 -0
  464. package/bundle/hkp-frontend/src/runtime/browser/services/SetRuntimeVariable.ts +65 -0
  465. package/bundle/hkp-frontend/src/runtime/browser/services/SetRuntimeVariableUI.tsx +50 -0
  466. package/bundle/hkp-frontend/src/runtime/browser/services/Sign.ts +182 -0
  467. package/bundle/hkp-frontend/src/runtime/browser/services/SignUI.tsx +55 -0
  468. package/bundle/hkp-frontend/src/runtime/browser/services/Smooth.ts +101 -0
  469. package/bundle/hkp-frontend/src/runtime/browser/services/SmoothUI.tsx +50 -0
  470. package/bundle/hkp-frontend/src/runtime/browser/services/Sort.ts +59 -0
  471. package/bundle/hkp-frontend/src/runtime/browser/services/SortUI.tsx +61 -0
  472. package/bundle/hkp-frontend/src/runtime/browser/services/Sound.ts +541 -0
  473. package/bundle/hkp-frontend/src/runtime/browser/services/SoundUI.tsx +183 -0
  474. package/bundle/hkp-frontend/src/runtime/browser/services/SpeechSynth.ts +56 -0
  475. package/bundle/hkp-frontend/src/runtime/browser/services/SpeechSynthUI.tsx +108 -0
  476. package/bundle/hkp-frontend/src/runtime/browser/services/Spotify.tsx +111 -0
  477. package/bundle/hkp-frontend/src/runtime/browser/services/SpotifyAPI.ts +113 -0
  478. package/bundle/hkp-frontend/src/runtime/browser/services/SpotifyComponents.tsx +104 -0
  479. package/bundle/hkp-frontend/src/runtime/browser/services/SpotifyUI.tsx +186 -0
  480. package/bundle/hkp-frontend/src/runtime/browser/services/Spread.ts +117 -0
  481. package/bundle/hkp-frontend/src/runtime/browser/services/SpreadUI.tsx +38 -0
  482. package/bundle/hkp-frontend/src/runtime/browser/services/Sql.ts +448 -0
  483. package/bundle/hkp-frontend/src/runtime/browser/services/SqlInspector.tsx +332 -0
  484. package/bundle/hkp-frontend/src/runtime/browser/services/SqlUI.tsx +226 -0
  485. package/bundle/hkp-frontend/src/runtime/browser/services/State.ts +149 -0
  486. package/bundle/hkp-frontend/src/runtime/browser/services/Stopper.ts +29 -0
  487. package/bundle/hkp-frontend/src/runtime/browser/services/Switch.tsx +419 -0
  488. package/bundle/hkp-frontend/src/runtime/browser/services/SwitchUI.tsx +171 -0
  489. package/bundle/hkp-frontend/src/runtime/browser/services/TickRuleDelta.ts +101 -0
  490. package/bundle/hkp-frontend/src/runtime/browser/services/TickRuleDeltaUI.tsx +56 -0
  491. package/bundle/hkp-frontend/src/runtime/browser/services/Timeline.ts +709 -0
  492. package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/TimelinePanel.tsx +79 -0
  493. package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/TimelineRows.tsx +574 -0
  494. package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/Transport.tsx +141 -0
  495. package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/index.tsx +119 -0
  496. package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/model.ts +425 -0
  497. package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/parts.tsx +708 -0
  498. package/bundle/hkp-frontend/src/runtime/browser/services/Timer.ts +15 -0
  499. package/bundle/hkp-frontend/src/runtime/browser/services/TimerUI.tsx +213 -0
  500. package/bundle/hkp-frontend/src/runtime/browser/services/TracksUI.tsx +81 -0
  501. package/bundle/hkp-frontend/src/runtime/browser/services/TriggerPad.ts +108 -0
  502. package/bundle/hkp-frontend/src/runtime/browser/services/TriggerPadUI.tsx +160 -0
  503. package/bundle/hkp-frontend/src/runtime/browser/services/UuidGenerator.ts +74 -0
  504. package/bundle/hkp-frontend/src/runtime/browser/services/UuidGeneratorUI.tsx +59 -0
  505. package/bundle/hkp-frontend/src/runtime/browser/services/WaveformDisplay.tsx +104 -0
  506. package/bundle/hkp-frontend/src/runtime/browser/services/WebsocketClient.ts +114 -0
  507. package/bundle/hkp-frontend/src/runtime/browser/services/WebsocketClientUI.tsx +42 -0
  508. package/bundle/hkp-frontend/src/runtime/browser/services/WorkflowBoardBuilder.ts +499 -0
  509. package/bundle/hkp-frontend/src/runtime/browser/services/WorkflowBoardBuilderUI.tsx +521 -0
  510. package/bundle/hkp-frontend/src/runtime/browser/services/XYPad.ts +68 -0
  511. package/bundle/hkp-frontend/src/runtime/browser/services/XYPadUI.tsx +274 -0
  512. package/bundle/hkp-frontend/src/runtime/browser/services/audio/audioHelper.ts +36 -0
  513. package/bundle/hkp-frontend/src/runtime/browser/services/base/Batcher.ts +76 -0
  514. package/bundle/hkp-frontend/src/runtime/browser/services/base/Buffer.ts +142 -0
  515. package/bundle/hkp-frontend/src/runtime/browser/services/base/Filter.ts +125 -0
  516. package/bundle/hkp-frontend/src/runtime/browser/services/base/GroupBy.ts +101 -0
  517. package/bundle/hkp-frontend/src/runtime/browser/services/base/Map.ts +543 -0
  518. package/bundle/hkp-frontend/src/runtime/browser/services/base/MatchFilter.ts +49 -0
  519. package/bundle/hkp-frontend/src/runtime/browser/services/base/Monitor.ts +75 -0
  520. package/bundle/hkp-frontend/src/runtime/browser/services/base/Reduce.ts +74 -0
  521. package/bundle/hkp-frontend/src/runtime/browser/services/base/Timer.ts +342 -0
  522. package/bundle/hkp-frontend/src/runtime/browser/services/base/bundle.js +7375 -0
  523. package/bundle/hkp-frontend/src/runtime/browser/services/base/eval.ts +208 -0
  524. package/bundle/hkp-frontend/src/runtime/browser/services/base/expressionEval.ts +223 -0
  525. package/bundle/hkp-frontend/src/runtime/browser/services/base/helpers.ts +10 -0
  526. package/bundle/hkp-frontend/src/runtime/browser/services/base/index.ts +37 -0
  527. package/bundle/hkp-frontend/src/runtime/browser/services/camera/videoCapture.ts +78 -0
  528. package/bundle/hkp-frontend/src/runtime/browser/services/changes-modes.ts +9 -0
  529. package/bundle/hkp-frontend/src/runtime/browser/services/colorpalette.ts +26 -0
  530. package/bundle/hkp-frontend/src/runtime/browser/services/detect/DetectionPreview.tsx +76 -0
  531. package/bundle/hkp-frontend/src/runtime/browser/services/detect/mediapipe.ts +34 -0
  532. package/bundle/hkp-frontend/src/runtime/browser/services/detect/models/README.md +14 -0
  533. package/bundle/hkp-frontend/src/runtime/browser/services/detect/models/blaze_face_short_range.tflite +0 -0
  534. package/bundle/hkp-frontend/src/runtime/browser/services/detect-modes.ts +7 -0
  535. package/bundle/hkp-frontend/src/runtime/browser/services/encode-modes.ts +10 -0
  536. package/bundle/hkp-frontend/src/runtime/browser/services/helpers.ts +300 -0
  537. package/bundle/hkp-frontend/src/runtime/browser/services/http-client-methods.ts +10 -0
  538. package/bundle/hkp-frontend/src/runtime/browser/services/sql-database.ts +594 -0
  539. package/bundle/hkp-frontend/src/runtime/browser/services/sql-dump.ts +192 -0
  540. package/bundle/hkp-frontend/src/runtime/browser/services/sql-inspect.ts +130 -0
  541. package/bundle/hkp-frontend/src/runtime/browser/services/sql-inspector.css +155 -0
  542. package/bundle/hkp-frontend/src/runtime/browser/services/sql-modes.ts +27 -0
  543. package/bundle/hkp-frontend/src/runtime/browser/services/tests/TESTING_APPROACH.md +169 -0
  544. package/bundle/hkp-frontend/src/runtime/browser/services/tests/__snapshots__/service-ui.future-enhancements.test.tsx.snap +5 -0
  545. package/bundle/hkp-frontend/src/runtime/browser/services/tests/audio-input-permissions.test.ts +42 -0
  546. package/bundle/hkp-frontend/src/runtime/browser/services/tests/audiolizeLanes.test.ts +70 -0
  547. package/bundle/hkp-frontend/src/runtime/browser/services/tests/browser-scopes.test.ts +305 -0
  548. package/bundle/hkp-frontend/src/runtime/browser/services/tests/cache-service.test.ts +53 -0
  549. package/bundle/hkp-frontend/src/runtime/browser/services/tests/camera-video-capture.test.ts +24 -0
  550. package/bundle/hkp-frontend/src/runtime/browser/services/tests/changes-service.test.ts +133 -0
  551. package/bundle/hkp-frontend/src/runtime/browser/services/tests/crypto-secrets.test.ts +196 -0
  552. package/bundle/hkp-frontend/src/runtime/browser/services/tests/detect-service.test.ts +119 -0
  553. package/bundle/hkp-frontend/src/runtime/browser/services/tests/download.test.ts +100 -0
  554. package/bundle/hkp-frontend/src/runtime/browser/services/tests/expressionEval.test.ts +117 -0
  555. package/bundle/hkp-frontend/src/runtime/browser/services/tests/fetcher-panel.test.tsx +35 -0
  556. package/bundle/hkp-frontend/src/runtime/browser/services/tests/fetcher-service.test.ts +80 -0
  557. package/bundle/hkp-frontend/src/runtime/browser/services/tests/filter-service.test.ts +37 -0
  558. package/bundle/hkp-frontend/src/runtime/browser/services/tests/github-secrets.test.ts +108 -0
  559. package/bundle/hkp-frontend/src/runtime/browser/services/tests/group-by-board.test.ts +42 -0
  560. package/bundle/hkp-frontend/src/runtime/browser/services/tests/hold-service.test.ts +199 -0
  561. package/bundle/hkp-frontend/src/runtime/browser/services/tests/http-client-service.test.ts +474 -0
  562. package/bundle/hkp-frontend/src/runtime/browser/services/tests/http-uploader-token-injector.test.ts +152 -0
  563. package/bundle/hkp-frontend/src/runtime/browser/services/tests/iterator-gather-service.test.ts +277 -0
  564. package/bundle/hkp-frontend/src/runtime/browser/services/tests/map-service.test.ts +1047 -0
  565. package/bundle/hkp-frontend/src/runtime/browser/services/tests/map-ui.test.tsx +70 -0
  566. package/bundle/hkp-frontend/src/runtime/browser/services/tests/nested-rhythm-board.test.ts +311 -0
  567. package/bundle/hkp-frontend/src/runtime/browser/services/tests/night-groove-board.test.ts +290 -0
  568. package/bundle/hkp-frontend/src/runtime/browser/services/tests/pad-groove-board.test.ts +230 -0
  569. package/bundle/hkp-frontend/src/runtime/browser/services/tests/peer-host-discovery.test.ts +77 -0
  570. package/bundle/hkp-frontend/src/runtime/browser/services/tests/peer-socket-discovery.test.ts +69 -0
  571. package/bundle/hkp-frontend/src/runtime/browser/services/tests/pipeline-names.test.ts +63 -0
  572. package/bundle/hkp-frontend/src/runtime/browser/services/tests/secret-canary.test.ts +121 -0
  573. package/bundle/hkp-frontend/src/runtime/browser/services/tests/service-modules.contract.test.ts +229 -0
  574. package/bundle/hkp-frontend/src/runtime/browser/services/tests/service-ui.advanced.test.tsx +670 -0
  575. package/bundle/hkp-frontend/src/runtime/browser/services/tests/service-ui.future-enhancements.test.tsx +219 -0
  576. package/bundle/hkp-frontend/src/runtime/browser/services/tests/service-ui.integration.test.tsx +432 -0
  577. package/bundle/hkp-frontend/src/runtime/browser/services/tests/service-ui.smoke.test.tsx +140 -0
  578. package/bundle/hkp-frontend/src/runtime/browser/services/tests/sound-service.test.ts +293 -0
  579. package/bundle/hkp-frontend/src/runtime/browser/services/tests/spread-service.test.ts +137 -0
  580. package/bundle/hkp-frontend/src/runtime/browser/services/tests/sql-dump.test.ts +172 -0
  581. package/bundle/hkp-frontend/src/runtime/browser/services/tests/sql-inspect.test.ts +163 -0
  582. package/bundle/hkp-frontend/src/runtime/browser/services/tests/sql-inspector.test.tsx +121 -0
  583. package/bundle/hkp-frontend/src/runtime/browser/services/tests/sql.test.ts +702 -0
  584. package/bundle/hkp-frontend/src/runtime/browser/services/tests/sub-service-in-block.test.tsx +84 -0
  585. package/bundle/hkp-frontend/src/runtime/browser/services/tests/switch-service.test.ts +254 -0
  586. package/bundle/hkp-frontend/src/runtime/browser/services/tests/timeline-board.test.ts +218 -0
  587. package/bundle/hkp-frontend/src/runtime/browser/services/tests/timeline-service.test.ts +1002 -0
  588. package/bundle/hkp-frontend/src/runtime/browser/services/tests/timeline-ui-model.test.ts +230 -0
  589. package/bundle/hkp-frontend/src/runtime/browser/services/tests/timeline-ui.test.tsx +371 -0
  590. package/bundle/hkp-frontend/src/runtime/browser/services/tests/timer-service.test.ts +902 -0
  591. package/bundle/hkp-frontend/src/runtime/browser/services/tests/tracks-service.test.ts +275 -0
  592. package/bundle/hkp-frontend/src/runtime/browser/services/tests/tracks-ui.test.tsx +126 -0
  593. package/bundle/hkp-frontend/src/runtime/browser/services/tests/triggerpad-recorder-board.test.ts +100 -0
  594. package/bundle/hkp-frontend/src/runtime/browser/services/tests/uuid-generator-service.test.ts +214 -0
  595. package/bundle/hkp-frontend/src/runtime/browser/services/timeline-core.ts +410 -0
  596. package/bundle/hkp-frontend/src/runtime/browser/services/workflow-prompt/DefaultWorkflowBoard.ts +90 -0
  597. package/bundle/hkp-frontend/src/runtime/browser/services/workflow-prompt/RefinementSession.ts +121 -0
  598. package/bundle/hkp-frontend/src/runtime/browser/services/workflow-prompt/SystemPromptCatalog.ts +245 -0
  599. package/bundle/hkp-frontend/src/runtime/browser/services/workflow-prompt/catalog/browser-service-specs.json +110 -0
  600. package/bundle/hkp-frontend/src/runtime/browser/services/workflow-prompt/catalog/runtime-capabilities.json +37 -0
  601. package/bundle/hkp-frontend/src/runtime/browser/tests/asset-service.test.ts +73 -0
  602. package/bundle/hkp-frontend/src/runtime/browser/tests/board-persistence.integration.test.tsx +391 -0
  603. package/bundle/hkp-frontend/src/runtime/browser/tests/browser-runtime.identity-binding.test.tsx +81 -0
  604. package/bundle/hkp-frontend/src/runtime/browser/tests/demo-boards.regression.test.tsx +386 -0
  605. package/bundle/hkp-frontend/src/runtime/browser/tests/failed-call-lifecycle.test.ts +56 -0
  606. package/bundle/hkp-frontend/src/runtime/browser/tests/http-client.integration.test.ts +146 -0
  607. package/bundle/hkp-frontend/src/runtime/browser/tests/nested-run.integration.test.ts +415 -0
  608. package/bundle/hkp-frontend/src/runtime/browser/tests/replay-next.test.ts +391 -0
  609. package/bundle/hkp-frontend/src/runtime/browser/tests/rerun.test.ts +71 -0
  610. package/bundle/hkp-frontend/src/runtime/browser/tests/run-no-input.test.ts +72 -0
  611. package/bundle/hkp-frontend/src/runtime/browser/tests/runtime-add.integration.test.tsx +256 -0
  612. package/bundle/hkp-frontend/src/runtime/browser/tests/runtime-operations.integration.test.tsx +310 -0
  613. package/bundle/hkp-frontend/src/runtime/browser/tests/runtime-processing.integration.test.ts +311 -0
  614. package/bundle/hkp-frontend/src/runtime/browser/tests/runtime-removal.integration.test.tsx +454 -0
  615. package/bundle/hkp-frontend/src/runtime/browser/tests/runtime-update.identity.integration.test.tsx +145 -0
  616. package/bundle/hkp-frontend/src/runtime/browser/tests/service-operations.integration.test.tsx +511 -0
  617. package/bundle/hkp-node/src/address.ts +72 -0
  618. package/bundle/hkp-node/src/assets.ts +582 -0
  619. package/bundle/hkp-node/src/auth.ts +369 -0
  620. package/bundle/hkp-node/src/callReport.ts +281 -0
  621. package/bundle/hkp-node/src/coordinator/auth.ts +85 -0
  622. package/bundle/hkp-node/src/coordinator/binaryFrame.ts +189 -0
  623. package/bundle/hkp-node/src/coordinator/boardStore.ts +0 -0
  624. package/bundle/hkp-node/src/coordinator/bridge.ts +242 -0
  625. package/bundle/hkp-node/src/coordinator/bridgeProtocol.ts +200 -0
  626. package/bundle/hkp-node/src/coordinator/coordinator.ts +594 -0
  627. package/bundle/hkp-node/src/coordinator/facadeAccess.ts +426 -0
  628. package/bundle/hkp-node/src/coordinator/fileBoardStore.ts +159 -0
  629. package/bundle/hkp-node/src/coordinator/frameLimits.ts +41 -0
  630. package/bundle/hkp-node/src/coordinator/index.ts +12 -0
  631. package/bundle/hkp-node/src/coordinator/join.ts +62 -0
  632. package/bundle/hkp-node/src/coordinator/logStore.ts +335 -0
  633. package/bundle/hkp-node/src/coordinator/members.ts +133 -0
  634. package/bundle/hkp-node/src/coordinator/mount.ts +124 -0
  635. package/bundle/hkp-node/src/coordinator/participantProtocol.ts +169 -0
  636. package/bundle/hkp-node/src/coordinator/participants.ts +666 -0
  637. package/bundle/hkp-node/src/coordinator/router.ts +416 -0
  638. package/bundle/hkp-node/src/coordinator/session.ts +1610 -0
  639. package/bundle/hkp-node/src/coordinator/types.ts +95 -0
  640. package/bundle/hkp-node/src/coordinatorLinks.ts +712 -0
  641. package/bundle/hkp-node/src/credentialedFetch.ts +122 -0
  642. package/bundle/hkp-node/src/egress.ts +344 -0
  643. package/bundle/hkp-node/src/expressionEval.ts +223 -0
  644. package/bundle/hkp-node/src/index.ts +470 -0
  645. package/bundle/hkp-node/src/instance.ts +172 -0
  646. package/bundle/hkp-node/src/mounts.ts +296 -0
  647. package/bundle/hkp-node/src/origins.ts +201 -0
  648. package/bundle/hkp-node/src/reset.ts +187 -0
  649. package/bundle/hkp-node/src/runtime.ts +1345 -0
  650. package/bundle/hkp-node/src/secrets.ts +292 -0
  651. package/bundle/hkp-node/src/server.ts +2120 -0
  652. package/bundle/hkp-node/src/services/asset.ts +150 -0
  653. package/bundle/hkp-node/src/services/claudeModels.ts +370 -0
  654. package/bundle/hkp-node/src/services/communication-dispatcher.ts +735 -0
  655. package/bundle/hkp-node/src/services/conversations.ts +914 -0
  656. package/bundle/hkp-node/src/services/database.ts +386 -0
  657. package/bundle/hkp-node/src/services/document-extract.ts +575 -0
  658. package/bundle/hkp-node/src/services/expression.ts +417 -0
  659. package/bundle/hkp-node/src/services/fileStore.ts +369 -0
  660. package/bundle/hkp-node/src/services/filesystem.ts +280 -0
  661. package/bundle/hkp-node/src/services/gather.ts +335 -0
  662. package/bundle/hkp-node/src/services/hold.ts +275 -0
  663. package/bundle/hkp-node/src/services/http-client.ts +525 -0
  664. package/bundle/hkp-node/src/services/http-server.ts +1222 -0
  665. package/bundle/hkp-node/src/services/imap-email.ts +627 -0
  666. package/bundle/hkp-node/src/services/injector.ts +135 -0
  667. package/bundle/hkp-node/src/services/iterator.ts +203 -0
  668. package/bundle/hkp-node/src/services/join.ts +226 -0
  669. package/bundle/hkp-node/src/services/map.ts +632 -0
  670. package/bundle/hkp-node/src/services/monitor.ts +128 -0
  671. package/bundle/hkp-node/src/services/nested-pipeline.ts +405 -0
  672. package/bundle/hkp-node/src/services/peer-server.ts +268 -0
  673. package/bundle/hkp-node/src/services/queue-store.ts +301 -0
  674. package/bundle/hkp-node/src/services/queue.ts +316 -0
  675. package/bundle/hkp-node/src/services/recordStore.ts +341 -0
  676. package/bundle/hkp-node/src/services/rss-parse.ts +281 -0
  677. package/bundle/hkp-node/src/services/rss.ts +392 -0
  678. package/bundle/hkp-node/src/services/smtp-email.ts +398 -0
  679. package/bundle/hkp-node/src/services/spread.ts +153 -0
  680. package/bundle/hkp-node/src/services/sql-dump.ts +192 -0
  681. package/bundle/hkp-node/src/services/sql.ts +429 -0
  682. package/bundle/hkp-node/src/services/stopper.ts +69 -0
  683. package/bundle/hkp-node/src/services/storage.ts +332 -0
  684. package/bundle/hkp-node/src/services/store.ts +609 -0
  685. package/bundle/hkp-node/src/services/stream.ts +367 -0
  686. package/bundle/hkp-node/src/services/sub-service.ts +600 -0
  687. package/bundle/hkp-node/src/services/telegram-listener.ts +263 -0
  688. package/bundle/hkp-node/src/services/telegram-sender.ts +136 -0
  689. package/bundle/hkp-node/src/services/text-generation.ts +1249 -0
  690. package/bundle/hkp-node/src/services/timer.ts +273 -0
  691. package/bundle/hkp-node/src/services/tracks.ts +381 -0
  692. package/bundle/hkp-node/src/services/websocket-reader.ts +364 -0
  693. package/bundle/hkp-node/src/types.ts +465 -0
  694. package/bundle/hkp-node/src/wsTickets.ts +77 -0
  695. package/bundle/hkp-node/src/yas.ts +95 -0
  696. package/bundle/hkp-python/src/hkp/__init__.py +1 -0
  697. package/bundle/hkp-python/src/hkp/__main__.py +241 -0
  698. package/bundle/hkp-python/src/hkp/address.py +56 -0
  699. package/bundle/hkp-python/src/hkp/assets.py +439 -0
  700. package/bundle/hkp-python/src/hkp/auth.py +252 -0
  701. package/bundle/hkp-python/src/hkp/binary_frame.py +100 -0
  702. package/bundle/hkp-python/src/hkp/call_report.py +201 -0
  703. package/bundle/hkp-python/src/hkp/coordinator_links.py +667 -0
  704. package/bundle/hkp-python/src/hkp/data.py +118 -0
  705. package/bundle/hkp-python/src/hkp/egress.py +310 -0
  706. package/bundle/hkp-python/src/hkp/mount.py +46 -0
  707. package/bundle/hkp-python/src/hkp/mounts.py +288 -0
  708. package/bundle/hkp-python/src/hkp/origins.py +169 -0
  709. package/bundle/hkp-python/src/hkp/runtime.py +1131 -0
  710. package/bundle/hkp-python/src/hkp/secrets.py +261 -0
  711. package/bundle/hkp-python/src/hkp/server.py +1714 -0
  712. package/bundle/hkp-python/src/hkp/services/__init__.py +0 -0
  713. package/bundle/hkp-python/src/hkp/services/asset.py +123 -0
  714. package/bundle/hkp-python/src/hkp/services/audio_encode.py +190 -0
  715. package/bundle/hkp-python/src/hkp/services/expression.py +942 -0
  716. package/bundle/hkp-python/src/hkp/services/gather.py +266 -0
  717. package/bundle/hkp-python/src/hkp/services/hold.py +246 -0
  718. package/bundle/hkp-python/src/hkp/services/http_client.py +444 -0
  719. package/bundle/hkp-python/src/hkp/services/http_server.py +790 -0
  720. package/bundle/hkp-python/src/hkp/services/iterator.py +141 -0
  721. package/bundle/hkp-python/src/hkp/services/join.py +173 -0
  722. package/bundle/hkp-python/src/hkp/services/map_service.py +452 -0
  723. package/bundle/hkp-python/src/hkp/services/monitor.py +99 -0
  724. package/bundle/hkp-python/src/hkp/services/nested_pipeline.py +278 -0
  725. package/bundle/hkp-python/src/hkp/services/skill_router.py +388 -0
  726. package/bundle/hkp-python/src/hkp/services/speech_to_text.py +186 -0
  727. package/bundle/hkp-python/src/hkp/services/spread.py +114 -0
  728. package/bundle/hkp-python/src/hkp/services/stopper.py +59 -0
  729. package/bundle/hkp-python/src/hkp/services/sub_service.py +417 -0
  730. package/bundle/hkp-python/src/hkp/services/text_generation.py +498 -0
  731. package/bundle/hkp-python/src/hkp/services/text_to_speech.py +237 -0
  732. package/bundle/hkp-python/src/hkp/services/timer.py +267 -0
  733. package/bundle/hkp-python/src/hkp/services/tracks.py +251 -0
  734. package/bundle/hkp-python/src/hkp/types.py +412 -0
  735. package/bundle/hkp-python/src/hkp/ws_tickets.py +54 -0
  736. package/bundle/hkp-python/src/hkp/yas.py +192 -0
  737. package/bundle/hkp-rt/lib/CMakeLists.txt +193 -0
  738. package/bundle/hkp-rt/lib/hkp-bundles/CMakeLists.txt +55 -0
  739. package/bundle/hkp-rt/lib/hkp-bundles/plugin_register.cpp +14 -0
  740. package/bundle/hkp-rt/lib/include/app.h +201 -0
  741. package/bundle/hkp-rt/lib/include/assets.h +239 -0
  742. package/bundle/hkp-rt/lib/include/auth.h +167 -0
  743. package/bundle/hkp-rt/lib/include/call_report.h +197 -0
  744. package/bundle/hkp-rt/lib/include/coordinator_links.h +127 -0
  745. package/bundle/hkp-rt/lib/include/discovery_hooks.h +29 -0
  746. package/bundle/hkp-rt/lib/include/egress.h +379 -0
  747. package/bundle/hkp-rt/lib/include/files.h +275 -0
  748. package/bundle/hkp-rt/lib/include/log_entry.h +131 -0
  749. package/bundle/hkp-rt/lib/include/mounts.h +139 -0
  750. package/bundle/hkp-rt/lib/include/origins.h +363 -0
  751. package/bundle/hkp-rt/lib/include/registry.h +119 -0
  752. package/bundle/hkp-rt/lib/include/secrets.h +414 -0
  753. package/bundle/hkp-rt/lib/include/server.h +122 -0
  754. package/bundle/hkp-rt/lib/include/service.h +190 -0
  755. package/bundle/hkp-rt/lib/include/slot_store.h +41 -0
  756. package/bundle/hkp-rt/lib/include/standalone_config.h +253 -0
  757. package/bundle/hkp-rt/lib/include/types/audio.h +3 -0
  758. package/bundle/hkp-rt/lib/include/types/data.h +152 -0
  759. package/bundle/hkp-rt/lib/include/types/message.h +56 -0
  760. package/bundle/hkp-rt/lib/include/types/models.h +154 -0
  761. package/bundle/hkp-rt/lib/include/types/owns_me.h +19 -0
  762. package/bundle/hkp-rt/lib/include/types/ringbuffer.h +86 -0
  763. package/bundle/hkp-rt/lib/include/types/types.h +33 -0
  764. package/bundle/hkp-rt/lib/include/types/validation.h +205 -0
  765. package/bundle/hkp-rt/lib/src/address.h +88 -0
  766. package/bundle/hkp-rt/lib/src/app.cpp +591 -0
  767. package/bundle/hkp-rt/lib/src/assets.cpp +686 -0
  768. package/bundle/hkp-rt/lib/src/binary_frame.h +154 -0
  769. package/bundle/hkp-rt/lib/src/common/expression.h +1481 -0
  770. package/bundle/hkp-rt/lib/src/common/inja.h +36 -0
  771. package/bundle/hkp-rt/lib/src/common/link_socket.cpp +437 -0
  772. package/bundle/hkp-rt/lib/src/common/link_socket.h +97 -0
  773. package/bundle/hkp-rt/lib/src/common/realtime_wakeup.h +120 -0
  774. package/bundle/hkp-rt/lib/src/common/stream_broadcast.h +250 -0
  775. package/bundle/hkp-rt/lib/src/common/string_util.h +31 -0
  776. package/bundle/hkp-rt/lib/src/common/websocket_client_session.cpp +217 -0
  777. package/bundle/hkp-rt/lib/src/common/websocket_client_session.h +37 -0
  778. package/bundle/hkp-rt/lib/src/common/websocket_config.h +119 -0
  779. package/bundle/hkp-rt/lib/src/common/websocket_protocol.h +32 -0
  780. package/bundle/hkp-rt/lib/src/common/websocket_server.cpp +82 -0
  781. package/bundle/hkp-rt/lib/src/common/websocket_server.h +57 -0
  782. package/bundle/hkp-rt/lib/src/common/websocket_server_listener.cpp +115 -0
  783. package/bundle/hkp-rt/lib/src/common/websocket_server_listener.h +35 -0
  784. package/bundle/hkp-rt/lib/src/common/websocket_server_session.cpp +172 -0
  785. package/bundle/hkp-rt/lib/src/common/websocket_server_session.h +53 -0
  786. package/bundle/hkp-rt/lib/src/common.h +13 -0
  787. package/bundle/hkp-rt/lib/src/coordinator_links.cpp +1142 -0
  788. package/bundle/hkp-rt/lib/src/discovery/discovery.cpp +707 -0
  789. package/bundle/hkp-rt/lib/src/discovery/discovery.h +89 -0
  790. package/bundle/hkp-rt/lib/src/http/auth.cpp +569 -0
  791. package/bundle/hkp-rt/lib/src/http/front_door.cpp +518 -0
  792. package/bundle/hkp-rt/lib/src/http/front_door.h +131 -0
  793. package/bundle/hkp-rt/lib/src/http/server.cpp +1539 -0
  794. package/bundle/hkp-rt/lib/src/http/ws_tickets.h +125 -0
  795. package/bundle/hkp-rt/lib/src/mount.h +63 -0
  796. package/bundle/hkp-rt/lib/src/process_context.h +335 -0
  797. package/bundle/hkp-rt/lib/src/process_depth.h +20 -0
  798. package/bundle/hkp-rt/lib/src/registry.cpp +233 -0
  799. package/bundle/hkp-rt/lib/src/runtime.cpp +986 -0
  800. package/bundle/hkp-rt/lib/src/runtime.h +225 -0
  801. package/bundle/hkp-rt/lib/src/runtime_host.h +180 -0
  802. package/bundle/hkp-rt/lib/src/service.cpp +315 -0
  803. package/bundle/hkp-rt/lib/src/services/asset.h +185 -0
  804. package/bundle/hkp-rt/lib/src/services/audio_encode.cpp +372 -0
  805. package/bundle/hkp-rt/lib/src/services/audio_encode.h +97 -0
  806. package/bundle/hkp-rt/lib/src/services/cache.h +362 -0
  807. package/bundle/hkp-rt/lib/src/services/cache_subservices.h +463 -0
  808. package/bundle/hkp-rt/lib/src/services/core_audio.cpp +384 -0
  809. package/bundle/hkp-rt/lib/src/services/core_audio.h +22 -0
  810. package/bundle/hkp-rt/lib/src/services/core_input.h +337 -0
  811. package/bundle/hkp-rt/lib/src/services/core_output.h +370 -0
  812. package/bundle/hkp-rt/lib/src/services/ffmpeg/detail.cpp +749 -0
  813. package/bundle/hkp-rt/lib/src/services/ffmpeg/detail.h +29 -0
  814. package/bundle/hkp-rt/lib/src/services/ffmpeg/ffmpeg.cpp +193 -0
  815. package/bundle/hkp-rt/lib/src/services/ffmpeg/ffmpeg.h +40 -0
  816. package/bundle/hkp-rt/lib/src/services/ffmpeg/types.h +120 -0
  817. package/bundle/hkp-rt/lib/src/services/fft_service.h +190 -0
  818. package/bundle/hkp-rt/lib/src/services/filesystem.h +309 -0
  819. package/bundle/hkp-rt/lib/src/services/filter.h +208 -0
  820. package/bundle/hkp-rt/lib/src/services/gather.h +272 -0
  821. package/bundle/hkp-rt/lib/src/services/hold.h +304 -0
  822. package/bundle/hkp-rt/lib/src/services/http_client.cpp +347 -0
  823. package/bundle/hkp-rt/lib/src/services/http_client.h +67 -0
  824. package/bundle/hkp-rt/lib/src/services/http_client_impl.h +45 -0
  825. package/bundle/hkp-rt/lib/src/services/http_client_template_examples.md +202 -0
  826. package/bundle/hkp-rt/lib/src/services/http_server/http_listener.cpp +130 -0
  827. package/bundle/hkp-rt/lib/src/services/http_server/http_listener.h +33 -0
  828. package/bundle/hkp-rt/lib/src/services/http_server/http_server.cpp +351 -0
  829. package/bundle/hkp-rt/lib/src/services/http_server/http_server.h +79 -0
  830. package/bundle/hkp-rt/lib/src/services/http_server/http_server_impl.cpp +230 -0
  831. package/bundle/hkp-rt/lib/src/services/http_server/http_server_impl.h +64 -0
  832. package/bundle/hkp-rt/lib/src/services/http_server/http_server_subservices.cpp +950 -0
  833. package/bundle/hkp-rt/lib/src/services/http_server/http_server_subservices.h +253 -0
  834. package/bundle/hkp-rt/lib/src/services/http_server/http_session.cpp +495 -0
  835. package/bundle/hkp-rt/lib/src/services/http_server/http_session.h +169 -0
  836. package/bundle/hkp-rt/lib/src/services/http_server/http_stream_listener.cpp +270 -0
  837. package/bundle/hkp-rt/lib/src/services/http_server/http_stream_listener.h +104 -0
  838. package/bundle/hkp-rt/lib/src/services/http_server/request_decode.h +164 -0
  839. package/bundle/hkp-rt/lib/src/services/if_service.h +210 -0
  840. package/bundle/hkp-rt/lib/src/services/ifft_service.h +198 -0
  841. package/bundle/hkp-rt/lib/src/services/injector.h +162 -0
  842. package/bundle/hkp-rt/lib/src/services/iterator.h +201 -0
  843. package/bundle/hkp-rt/lib/src/services/map.h +641 -0
  844. package/bundle/hkp-rt/lib/src/services/monitor.h +161 -0
  845. package/bundle/hkp-rt/lib/src/services/mp3_frames.h +119 -0
  846. package/bundle/hkp-rt/lib/src/services/mp4_to_wav.cpp +281 -0
  847. package/bundle/hkp-rt/lib/src/services/mp4_to_wav.h +263 -0
  848. package/bundle/hkp-rt/lib/src/services/peer_server/peer_registry.cpp +88 -0
  849. package/bundle/hkp-rt/lib/src/services/peer_server/peer_registry.h +43 -0
  850. package/bundle/hkp-rt/lib/src/services/peer_server/peer_server.h +163 -0
  851. package/bundle/hkp-rt/lib/src/services/peer_server/peer_server_listener.cpp +133 -0
  852. package/bundle/hkp-rt/lib/src/services/peer_server/peer_server_listener.h +46 -0
  853. package/bundle/hkp-rt/lib/src/services/peer_server/peer_server_session.cpp +165 -0
  854. package/bundle/hkp-rt/lib/src/services/peer_server/peer_server_session.h +44 -0
  855. package/bundle/hkp-rt/lib/src/services/peer_server/peer_ws_session.cpp +184 -0
  856. package/bundle/hkp-rt/lib/src/services/peer_server/peer_ws_session.h +55 -0
  857. package/bundle/hkp-rt/lib/src/services/receiver_session.h +172 -0
  858. package/bundle/hkp-rt/lib/src/services/root_certificates.h +3959 -0
  859. package/bundle/hkp-rt/lib/src/services/sha256_openssl.h +49 -0
  860. package/bundle/hkp-rt/lib/src/services/speech_http.h +306 -0
  861. package/bundle/hkp-rt/lib/src/services/speech_to_text.cpp +595 -0
  862. package/bundle/hkp-rt/lib/src/services/speech_to_text.h +69 -0
  863. package/bundle/hkp-rt/lib/src/services/static.cpp +45 -0
  864. package/bundle/hkp-rt/lib/src/services/static.h +41 -0
  865. package/bundle/hkp-rt/lib/src/services/stopper.h +72 -0
  866. package/bundle/hkp-rt/lib/src/services/sub_service.h +286 -0
  867. package/bundle/hkp-rt/lib/src/services/text_generation.cpp +1132 -0
  868. package/bundle/hkp-rt/lib/src/services/text_generation.h +71 -0
  869. package/bundle/hkp-rt/lib/src/services/text_to_speech.cpp +853 -0
  870. package/bundle/hkp-rt/lib/src/services/text_to_speech.h +97 -0
  871. package/bundle/hkp-rt/lib/src/services/timer.h +300 -0
  872. package/bundle/hkp-rt/lib/src/services/tracks.h +397 -0
  873. package/bundle/hkp-rt/lib/src/services/tts_inflect.cpp +149 -0
  874. package/bundle/hkp-rt/lib/src/services/tts_inflect.h +82 -0
  875. package/bundle/hkp-rt/lib/src/services/wav_reader.h +213 -0
  876. package/bundle/hkp-rt/lib/src/services/websocket_client_service.h +182 -0
  877. package/bundle/hkp-rt/lib/src/services/websocket_reader.h +148 -0
  878. package/bundle/hkp-rt/lib/src/services/websocket_server_service.h +196 -0
  879. package/bundle/hkp-rt/lib/src/services/websocket_writer.cpp +641 -0
  880. package/bundle/hkp-rt/lib/src/services/websocket_writer.h +117 -0
  881. package/bundle/hkp-rt/lib/src/sub_runtime.cpp +234 -0
  882. package/bundle/hkp-rt/lib/src/sub_runtime.h +160 -0
  883. package/bundle/hkp-rt/lib/src/types/data.cpp +237 -0
  884. package/bundle/hkp-rt/lib/src/types/message.cpp +148 -0
  885. package/bundle/hkp-rt/lib/src/types/models.cpp +83 -0
  886. package/bundle/hkp-rt/lib/src/types/ringbuffer.cpp +336 -0
  887. package/bundle/hkp-rt/lib/src/uuid.h +12 -0
  888. package/bundle/readymade-mcp-commits.json +5 -0
  889. package/dist/cli.d.ts +3 -0
  890. package/dist/cli.d.ts.map +1 -0
  891. package/dist/cli.js +79 -0
  892. package/dist/git.d.ts +8 -0
  893. package/dist/git.d.ts.map +1 -0
  894. package/dist/git.js +53 -0
  895. package/dist/http.d.ts +17 -0
  896. package/dist/http.d.ts.map +1 -0
  897. package/dist/http.js +130 -0
  898. package/dist/index.d.ts +7 -0
  899. package/dist/index.d.ts.map +1 -0
  900. package/dist/index.js +6 -0
  901. package/dist/repository.d.ts +49 -0
  902. package/dist/repository.d.ts.map +1 -0
  903. package/dist/repository.js +452 -0
  904. package/dist/schemas.d.ts +156 -0
  905. package/dist/schemas.d.ts.map +1 -0
  906. package/dist/schemas.js +43 -0
  907. package/dist/server.d.ts +4 -0
  908. package/dist/server.d.ts.map +1 -0
  909. package/dist/server.js +83 -0
  910. package/dist/types.d.ts +63 -0
  911. package/dist/types.d.ts.map +1 -0
  912. package/dist/types.js +20 -0
  913. package/dist/validation.d.ts +9 -0
  914. package/dist/validation.d.ts.map +1 -0
  915. package/dist/validation.js +101 -0
  916. package/package.json +52 -4
@@ -0,0 +1,1714 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import secrets
6
+ from concurrent.futures import ThreadPoolExecutor
7
+ from dataclasses import dataclass, replace
8
+ from typing import Any, Callable, Coroutine
9
+
10
+ from aiohttp import WSMsgType, web
11
+
12
+ from .auth import (
13
+ AllowedOrigins,
14
+ AuthConfig,
15
+ AuthenticatedUser,
16
+ Authenticator,
17
+ is_origin_allowed,
18
+ owner_key_of,
19
+ )
20
+ from .origins import (
21
+ DEFAULT_ORIGINS,
22
+ admits_without_credential,
23
+ is_forwarded,
24
+ allows_origin,
25
+ allows_origin_without_credential,
26
+ )
27
+ from .call_report import outgoing_payload
28
+ from .data import BinaryData, FloatRingBuffer, NullData, TextData, UndefinedData
29
+ from .mounts import MOUNT_PREFIX, MountRegistry, RuntimeMounts
30
+ from .ws_tickets import TICKET_TTL_SECONDS, WsTickets
31
+ from .runtime import set_server_loop
32
+ from .secrets import SecretEntry, read_secrets_payload, referenced_secrets
33
+ from .assets import read_assets_payload
34
+ from .services.asset import ASSET_DESCRIPTOR, AssetService
35
+ from .coordinator_links import (
36
+ CoordinatorLinks,
37
+ FileLinkStore,
38
+ LinkRecord,
39
+ MemoryLinkStore,
40
+ )
41
+ from .runtime import (
42
+ board_space,
43
+ context_for_client,
44
+ context_from_link,
45
+ context_to_wire,
46
+ new_run,
47
+ HostedRuntime,
48
+ HostedServiceFactory,
49
+ LOG_LEVELS,
50
+ RuntimeApp,
51
+ TenantRuntimes,
52
+ )
53
+ from .yas import (
54
+ MessagePurpose,
55
+ YasError,
56
+ deserialize_message,
57
+ is_yas_message,
58
+ serialize_message,
59
+ )
60
+ from .services.http_server import (
61
+ HTTP_SERVER_SUBSERVICES_DESCRIPTOR,
62
+ HttpServerSubservicesService,
63
+ )
64
+ from .services.map_service import MAP_DESCRIPTOR, MapService
65
+ from .services.http_client import HTTP_CLIENT_DESCRIPTOR, HttpClientService
66
+ from .services.stopper import STOPPER_DESCRIPTOR, StopperService
67
+ from .services.hold import HOLD_DESCRIPTOR, HoldService
68
+ from .services.monitor import MONITOR_DESCRIPTOR, MonitorService
69
+ from .services.speech_to_text import SPEECH_TO_TEXT_DESCRIPTOR, SpeechToTextService
70
+ from .services.text_generation import TEXT_GENERATION_DESCRIPTOR, TextGenerationService
71
+ from .services.skill_router import SKILL_ROUTER_DESCRIPTOR, SkillRouterService
72
+ from .services.text_to_speech import TEXT_TO_SPEECH_DESCRIPTOR, TextToSpeechService
73
+ from .services.audio_encode import AUDIO_ENCODE_DESCRIPTOR, AudioEncodeService
74
+ from .services.sub_service import SUB_SERVICE_DESCRIPTOR, SubService
75
+ from .services.join import JOIN_DESCRIPTOR, JoinService
76
+ from .services.tracks import TRACKS_DESCRIPTOR, TracksService
77
+ from .services.iterator import ITERATOR_DESCRIPTOR, IteratorService
78
+ from .services.gather import GATHER_DESCRIPTOR, GatherService
79
+ from .services.spread import SPREAD_DESCRIPTOR, SpreadService
80
+ from .services.timer import (
81
+ TIMER_DESCRIPTOR,
82
+ TIMER_LEGACY_SERVICE_ID,
83
+ TimerService,
84
+ )
85
+ from .types import (
86
+ ProcessContext,
87
+ JsonRecord,
88
+ LogEntry,
89
+ RuntimeConfiguration,
90
+ RuntimeNotification,
91
+ ServiceConfiguration,
92
+ )
93
+
94
+
95
+ # Typed request-storage key on aiohttp >= 3.13; plain string on older versions.
96
+ _AUTHENTICATED_USER_KEY: Any = (
97
+ web.RequestKey("authenticated_user", AuthenticatedUser)
98
+ if hasattr(web, "RequestKey")
99
+ else "authenticated_user"
100
+ )
101
+
102
+
103
+ #: Largest accepted request body on a public service endpoint (25 MB).
104
+ DEFAULT_MAX_REQUEST_BODY_BYTES = 25 * 1024 * 1024
105
+
106
+ #: Which runtime server this is, reported beside the runtimes so a client can
107
+ #: tell remote runtimes apart without reading their address.
108
+ RUNTIME_SERVER_KIND = "python"
109
+
110
+
111
+ def _tenant_key(owner: str, runtime_id: str) -> str:
112
+ """Runtime ids are unique per tenant, not globally, so anything keyed by
113
+ runtime outside a tenant view (socket sets) must be keyed by both. NUL
114
+ cannot occur in either part, so the join is unambiguous."""
115
+ return f"{owner}\x00{runtime_id}"
116
+
117
+
118
+ @dataclass
119
+ class SessionToken:
120
+ """A coordinator session token, bound to the user it was minted for and the
121
+ runtime it grants access to."""
122
+
123
+ sub: str
124
+ runtime_id: str
125
+
126
+
127
+ class _QuotaError(Exception):
128
+ """Refused for being over a per-tenant limit; says what to tell the caller."""
129
+
130
+
131
+ class RuntimeServer:
132
+ #: Which runtime server this is, as a coordinator link reports it.
133
+ kind = RUNTIME_SERVER_KIND
134
+
135
+ def __init__(self, options: dict[str, Any]) -> None:
136
+ self._external_host: str = options.get("external_host", "127.0.0.1")
137
+ self._allowed_origins: AllowedOrigins = options.get(
138
+ "allowed_origins", DEFAULT_ORIGINS
139
+ )
140
+ # Tests and local dev default to no auth; __main__.py always resolves an
141
+ # explicit config and fails closed for non-loopback binds (see
142
+ # resolve_server_auth_config).
143
+ self._auth_config: AuthConfig = options.get("auth") or AuthConfig(mode="none")
144
+ # Coordinator session tokens this runtime has issued (see POST
145
+ # .../session-token). Opaque, in-memory, and bound to the minting user —
146
+ # so they resolve back to a real `sub`, not an unscoped superuser. They
147
+ # live only as long as this process: if the runtime dies, the
148
+ # coordinator must re-provision (which needs a live user JWT).
149
+ self._session_tokens: dict[str, SessionToken] = {}
150
+ # Per-tenant limits. Runtimes, services and timers all consume resources
151
+ # on a shared host, so without a cap one tenant can degrade the server
152
+ # for everyone. 0 / unset means unlimited, which is the right default
153
+ # for a single-user instance and for local development.
154
+ quotas: dict[str, Any] = options.get("quotas") or {}
155
+ self._max_runtimes_per_user: int = quotas.get("max_runtimes_per_user") or 0
156
+ self._max_services_per_runtime: int = (
157
+ quotas.get("max_services_per_runtime") or 0
158
+ )
159
+ self._min_timer_interval_ms: int = quotas.get("min_timer_interval_ms") or 0
160
+ # Unlike the other quotas this defaults to a real value rather than
161
+ # unlimited: service endpoints take no token, so "off" would make the
162
+ # dangerous choice the automatic one.
163
+ self._max_request_body_bytes: int = (
164
+ quotas["max_request_body_bytes"]
165
+ if "max_request_body_bytes" in quotas
166
+ else DEFAULT_MAX_REQUEST_BODY_BYTES
167
+ )
168
+ # Builds the authenticator, defaulting to a JWKS-backed one for `auth`.
169
+ # Overriding it replaces only how a raw token is verified — the server
170
+ # still resolves its own session tokens first, by passing the resolver it
171
+ # owns into whatever this returns.
172
+ build_authenticator = options.get("build_authenticator")
173
+ # What a browser opens a WebSocket with instead of its token; see
174
+ # ws_tickets.py.
175
+ self._ws_tickets: WsTickets[AuthenticatedUser] = WsTickets()
176
+ self._authenticator = (
177
+ build_authenticator(self._resolve_session_token)
178
+ if build_authenticator
179
+ else Authenticator(
180
+ self._auth_config,
181
+ resolve_opaque_token=self._resolve_session_token,
182
+ )
183
+ )
184
+
185
+ factories = {
186
+ MONITOR_DESCRIPTOR.service_id: HostedServiceFactory(
187
+ MONITOR_DESCRIPTOR,
188
+ lambda cfg, _cs: MonitorService(cfg),
189
+ ),
190
+ MAP_DESCRIPTOR.service_id: HostedServiceFactory(
191
+ MAP_DESCRIPTOR,
192
+ lambda cfg, _cs: MapService(cfg),
193
+ ),
194
+ SUB_SERVICE_DESCRIPTOR.service_id: HostedServiceFactory(
195
+ SUB_SERVICE_DESCRIPTOR,
196
+ lambda cfg, cs: SubService(cfg, cs),
197
+ ),
198
+ JOIN_DESCRIPTOR.service_id: HostedServiceFactory(
199
+ JOIN_DESCRIPTOR,
200
+ lambda cfg, cs: JoinService(cfg, cs),
201
+ ),
202
+ TRACKS_DESCRIPTOR.service_id: HostedServiceFactory(
203
+ TRACKS_DESCRIPTOR,
204
+ lambda cfg, cs: TracksService(cfg, cs),
205
+ ),
206
+ ITERATOR_DESCRIPTOR.service_id: HostedServiceFactory(
207
+ ITERATOR_DESCRIPTOR,
208
+ lambda cfg, cs: IteratorService(cfg, cs),
209
+ ),
210
+ GATHER_DESCRIPTOR.service_id: HostedServiceFactory(
211
+ GATHER_DESCRIPTOR,
212
+ lambda cfg, cs: GatherService(cfg, cs),
213
+ ),
214
+ SPREAD_DESCRIPTOR.service_id: HostedServiceFactory(
215
+ SPREAD_DESCRIPTOR,
216
+ lambda cfg, _cs: SpreadService(cfg),
217
+ ),
218
+ HTTP_SERVER_SUBSERVICES_DESCRIPTOR.service_id: HostedServiceFactory(
219
+ HTTP_SERVER_SUBSERVICES_DESCRIPTOR,
220
+ lambda cfg, cs: HttpServerSubservicesService(
221
+ cfg, cs, self._max_request_body_bytes
222
+ ),
223
+ ),
224
+ HTTP_CLIENT_DESCRIPTOR.service_id: HostedServiceFactory(
225
+ HTTP_CLIENT_DESCRIPTOR,
226
+ lambda cfg, _cs: HttpClientService(cfg),
227
+ ),
228
+ STOPPER_DESCRIPTOR.service_id: HostedServiceFactory(
229
+ STOPPER_DESCRIPTOR,
230
+ lambda cfg, _cs: StopperService(cfg),
231
+ ),
232
+ HOLD_DESCRIPTOR.service_id: HostedServiceFactory(
233
+ HOLD_DESCRIPTOR,
234
+ lambda cfg, _cs: HoldService(cfg),
235
+ ),
236
+ ASSET_DESCRIPTOR.service_id: HostedServiceFactory(
237
+ ASSET_DESCRIPTOR,
238
+ lambda cfg, _cs: AssetService(cfg),
239
+ ),
240
+ TIMER_DESCRIPTOR.service_id: HostedServiceFactory(
241
+ TIMER_DESCRIPTOR,
242
+ lambda cfg, _cs: TimerService(cfg, self._min_timer_interval_ms),
243
+ ),
244
+ # Same service, under the id it answered to before it matched
245
+ # hkp-node and hkp-rt. Boards saved against the old id still load;
246
+ # the registry advertises only the canonical one.
247
+ TIMER_LEGACY_SERVICE_ID: HostedServiceFactory(
248
+ TIMER_DESCRIPTOR,
249
+ lambda cfg, _cs: TimerService(cfg, self._min_timer_interval_ms),
250
+ ),
251
+ SPEECH_TO_TEXT_DESCRIPTOR.service_id: HostedServiceFactory(
252
+ SPEECH_TO_TEXT_DESCRIPTOR,
253
+ lambda cfg, _cs: SpeechToTextService(cfg),
254
+ ),
255
+ TEXT_GENERATION_DESCRIPTOR.service_id: HostedServiceFactory(
256
+ TEXT_GENERATION_DESCRIPTOR,
257
+ lambda cfg, _cs: TextGenerationService(cfg),
258
+ ),
259
+ TEXT_TO_SPEECH_DESCRIPTOR.service_id: HostedServiceFactory(
260
+ TEXT_TO_SPEECH_DESCRIPTOR,
261
+ lambda cfg, _cs: TextToSpeechService(cfg),
262
+ ),
263
+ AUDIO_ENCODE_DESCRIPTOR.service_id: HostedServiceFactory(
264
+ AUDIO_ENCODE_DESCRIPTOR,
265
+ lambda cfg, _cs: AudioEncodeService(cfg),
266
+ ),
267
+ SKILL_ROUTER_DESCRIPTOR.service_id: HostedServiceFactory(
268
+ SKILL_ROUTER_DESCRIPTOR,
269
+ lambda cfg, _cs: SkillRouterService(cfg),
270
+ ),
271
+ }
272
+
273
+ # Public service endpoints. Declared before the runtime app because
274
+ # runtimes hand mounts to their services as they are created.
275
+ # Keys the derivation of public endpoint addresses. Given none, the
276
+ # registry draws one for this process, so addresses work but change on
277
+ # restart; `__main__` persists one so they do not.
278
+ self._mounts = MountRegistry(
279
+ self._public_mount_url, options.get("mount_secret")
280
+ )
281
+ self.runtime_app = RuntimeApp(
282
+ factories,
283
+ mounts_for=lambda owner, runtime_id, space: RuntimeMounts(
284
+ mount=lambda service_uuid, handler, board_name="", mount_name=None: (
285
+ self._mounts.register(
286
+ owner,
287
+ runtime_id,
288
+ service_uuid,
289
+ handler,
290
+ board_name=board_name,
291
+ mount_name=mount_name,
292
+ space=space,
293
+ )
294
+ )
295
+ ),
296
+ )
297
+ # Keyed by _tenant_key(owner, runtime_id), not runtime id alone: ids are
298
+ # unique per tenant, not globally.
299
+ self._runtime_sockets: dict[str, set[web.WebSocketResponse]] = {}
300
+ # This server's connections to coordinators, and the tickets it keeps
301
+ # to reconnect with. A path keeps them on disk; absent or empty means
302
+ # memory, so the links work and are not re-established after a restart.
303
+ link_store = options.get("coordinator_links")
304
+ self.coordinator_links = CoordinatorLinks(
305
+ self,
306
+ (
307
+ (FileLinkStore(link_store) if link_store else MemoryLinkStore())
308
+ if isinstance(link_store, str) or link_store is None
309
+ else link_store
310
+ ),
311
+ options.get("coordinator_link_options"),
312
+ spawn=self._spawn,
313
+ dumps=lambda value: json.dumps(value, default=_json_placeholder),
314
+ )
315
+ self._app = self._build_app()
316
+ self._runner: web.AppRunner | None = None
317
+ self._port = 0
318
+ self._loop: asyncio.AbstractEventLoop | None = None
319
+ # Single worker so pipeline processing stays serialized (services are
320
+ # not thread-safe) while long-running work (ML inference) does not
321
+ # block the event loop.
322
+ self._process_executor = ThreadPoolExecutor(
323
+ max_workers=1, thread_name_prefix="hkp-process"
324
+ )
325
+
326
+ # ── Lifecycle ──────────────────────────────────────────────────────────────
327
+
328
+ async def start(self, port: int = 0, host: str = "127.0.0.1") -> dict[str, Any]:
329
+ self._loop = asyncio.get_running_loop()
330
+ # Services schedule their own work — a request, a timer — from a pass
331
+ # that runs on a worker thread, where there is no loop to schedule on.
332
+ set_server_loop(self._loop)
333
+ self._runner = web.AppRunner(self._app)
334
+ await self._runner.setup()
335
+ site = web.TCPSite(self._runner, host, port)
336
+ await site.start()
337
+
338
+ if site._server and site._server.sockets:
339
+ self._port = site._server.sockets[0].getsockname()[1]
340
+
341
+ base_url = f"http://{self._external_host}:{self._port}"
342
+ return {"host": host, "port": self._port, "base_url": base_url}
343
+
344
+ async def stop(self) -> None:
345
+ await self.coordinator_links.stop()
346
+ # A copy: a socket's handler drops its entry while its close is awaited.
347
+ for sockets in list(self._runtime_sockets.values()):
348
+ for ws in list(sockets):
349
+ await ws.close()
350
+ self._runtime_sockets.clear()
351
+
352
+ if self._runner:
353
+ await self._runner.cleanup()
354
+ self._runner = None
355
+
356
+ self._process_executor.shutdown(wait=False, cancel_futures=True)
357
+
358
+ # ── Auth ───────────────────────────────────────────────────────────────────
359
+
360
+ def _at_quota(self, count: int, limit: int) -> bool:
361
+ """True when adding one more to ``count`` would pass ``limit``."""
362
+ return limit > 0 and count >= limit
363
+
364
+ def _exceeds_quota(self, count: int, limit: int) -> bool:
365
+ """True when ``count`` items is already more than ``limit`` allows."""
366
+ return limit > 0 and count > limit
367
+
368
+ def _tenant_of(self, request: web.Request) -> TenantRuntimes:
369
+ """The caller's runtime namespace. Every route resolves through it."""
370
+ return self.runtime_app.for_owner(self._owner_of(request))
371
+
372
+ def _owner_of(self, request: web.Request) -> str:
373
+ return owner_key_of(request.get(_AUTHENTICATED_USER_KEY))
374
+
375
+ def _resolve_session_token(self, token: str) -> AuthenticatedUser | None:
376
+ session = self._session_tokens.get(token)
377
+ return AuthenticatedUser(sub=session.sub) if session else None
378
+
379
+ def _purge_session_tokens(self, owner: str, runtime_id: str) -> None:
380
+ """Drop any session tokens a runtime issued, so a dead runtime's tokens
381
+ can't linger as valid credentials."""
382
+ for token, info in list(self._session_tokens.items()):
383
+ if info.sub == owner and info.runtime_id == runtime_id:
384
+ del self._session_tokens[token]
385
+
386
+ def _purge_owner_session_tokens(self, owner: str) -> None:
387
+ for token, info in list(self._session_tokens.items()):
388
+ if info.sub == owner:
389
+ del self._session_tokens[token]
390
+
391
+ @web.middleware
392
+ async def _auth_middleware(self, request: web.Request, handler: Any) -> web.Response:
393
+ """Authenticate every request with the same rules as hkp-node: HTTP
394
+ routes take the token from the Authorization header; WebSocket
395
+ handshakes may also carry it as ?access_token= (browsers can't set
396
+ headers on a WS upgrade) and are additionally Origin-checked (CSWSH
397
+ protection)."""
398
+ if request.method == "OPTIONS":
399
+ # CORS preflights are unauthenticated by design.
400
+ return await handler(request)
401
+
402
+ # Service endpoints exist to be called by outside parties (webhooks,
403
+ # uploads) that hold no token; their unguessable mount id is what gates
404
+ # access. Checked before anything else so no auth failure can shadow them.
405
+ if request.path.startswith(f"{MOUNT_PREFIX}/"):
406
+ return await handler(request)
407
+
408
+ is_upgrade = request.headers.get("Upgrade", "").strip().lower() == "websocket"
409
+
410
+ # A server without auth lets a request in for where it comes from, so
411
+ # it asks that this is not a foreign page; see origins.py. Answered
412
+ # with nothing a page could read — no CORS headers are set for it — so
413
+ # to the page that sent it this is what a server that is not there
414
+ # looks like.
415
+ if self._no_auth and not admits_without_credential(
416
+ request.headers.get("Origin"),
417
+ request.headers.get("Sec-Fetch-Site"),
418
+ request.headers.get("Host"),
419
+ self._allowed_origins,
420
+ [self._external_host],
421
+ forwarded=is_forwarded(request.headers),
422
+ ):
423
+ return web.Response(status=403)
424
+
425
+ header = request.headers.get("Authorization")
426
+ token = header[7:] if header and header.startswith("Bearer ") else None
427
+ if is_upgrade:
428
+ if not is_origin_allowed(
429
+ request.headers.get("Origin"), self._allowed_origins
430
+ ):
431
+ raise web.HTTPForbidden()
432
+ # A browser cannot set a header on a handshake, so it presents a
433
+ # ticket it was given for its token (ws_tickets.py). A client that
434
+ # can sends the token itself; ``?access_token=`` is what browsers
435
+ # sent before tickets, and is still taken, though it puts the token
436
+ # where logs keep it.
437
+ ticket = request.query.get("ticket")
438
+ if ticket is not None:
439
+ holder = self._ws_tickets.redeem(ticket)
440
+ if holder is None:
441
+ raise web.HTTPUnauthorized()
442
+ request[_AUTHENTICATED_USER_KEY] = holder
443
+ return await handler(request)
444
+ token = token or request.query.get("access_token")
445
+
446
+ user = await self._authenticator.authorize_owner(token)
447
+ if user is None:
448
+ raise web.HTTPUnauthorized()
449
+ request[_AUTHENTICATED_USER_KEY] = user
450
+ return await handler(request)
451
+
452
+ @property
453
+ def _no_auth(self) -> bool:
454
+ return self._auth_config.mode == "none"
455
+
456
+ def _page_may_read(self, origin: str | None) -> bool:
457
+ """Which page may read an answer: the one asking, when it is one this
458
+ server allows. Without auth there are no credentials, so ``*`` — any
459
+ page, with one — allows nobody."""
460
+ if self._no_auth:
461
+ return allows_origin_without_credential(origin, self._allowed_origins)
462
+ return allows_origin(origin, self._allowed_origins)
463
+
464
+ @web.middleware
465
+ async def _cors_middleware(self, request: web.Request, handler: Any) -> web.Response:
466
+ """States who may read an answer, in one place for every response — a
467
+ handler's, an error's, a preflight's — so that none can answer a page
468
+ the server does not allow. Mounts are left alone: what they send is
469
+ their pipeline's to decide."""
470
+ if request.path.startswith(f"{MOUNT_PREFIX}/"):
471
+ return await handler(request)
472
+
473
+ origin = request.headers.get("Origin")
474
+ allowed = bool(origin) and self._page_may_read(origin)
475
+
476
+ if request.method == "OPTIONS" and "Access-Control-Request-Method" in request.headers:
477
+ if not allowed:
478
+ return web.Response(status=403)
479
+ response = web.Response(status=200)
480
+ # PATCH: the page switching Capture mode on a runtime while it runs
481
+ # (PATCH /runtimes/{id}/state), which nothing else sends from a browser.
482
+ response.headers["Access-Control-Allow-Methods"] = "GET, POST, PATCH, DELETE, OPTIONS"
483
+ response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
484
+ _allow_origin(response, origin)
485
+ return response
486
+
487
+ try:
488
+ response = await handler(request)
489
+ except web.HTTPException as error:
490
+ if allowed:
491
+ _allow_origin(error, origin)
492
+ raise
493
+ if allowed and not response.prepared:
494
+ _allow_origin(response, origin)
495
+ exposed = [name for name in response.headers if name.lower() != "vary"]
496
+ if exposed:
497
+ response.headers["Access-Control-Expose-Headers"] = ", ".join(exposed)
498
+ return response
499
+
500
+ # ── App construction ───────────────────────────────────────────────────────
501
+
502
+ def _build_app(self) -> web.Application:
503
+ # Error middleware is outermost so it also renders auth failures.
504
+ app = web.Application(
505
+ middlewares=[
506
+ _error_middleware,
507
+ self._cors_middleware,
508
+ self._auth_middleware,
509
+ ]
510
+ )
511
+
512
+ app.router.add_route("*", f"{MOUNT_PREFIX}/{{mount_id}}", self._mounts.handle)
513
+ app.router.add_route(
514
+ "*", f"{MOUNT_PREFIX}/{{mount_id}}/{{sub_path:.*}}", self._mounts.handle
515
+ )
516
+
517
+ app.router.add_post("/coordinator-links", self._post_coordinator_link)
518
+ app.router.add_get("/coordinator-links", self._get_coordinator_links)
519
+ app.router.add_delete(
520
+ "/coordinator-links/{board_name}/{runtime_id}",
521
+ self._delete_coordinator_link,
522
+ )
523
+
524
+ app.router.add_get("/runtimes", self._get_runtimes)
525
+ app.router.add_post("/ws-tickets", self._post_ws_ticket)
526
+ app.router.add_post("/runtimes", self._post_runtimes)
527
+ app.router.add_delete("/runtimes", self._delete_runtimes)
528
+
529
+ app.router.add_get("/runtimes/{runtime_id}", self._get_runtime)
530
+ app.router.add_delete("/runtimes/{runtime_id}", self._delete_runtime)
531
+ app.router.add_post(
532
+ "/runtimes/{runtime_id}/session-token", self._mint_session_token
533
+ )
534
+ app.router.add_post("/runtimes/{runtime_id}/secrets", self._post_secrets)
535
+ app.router.add_post("/runtimes/{runtime_id}/assets", self._post_assets)
536
+ app.router.add_get(
537
+ "/runtimes/{runtime_id}/assets/{asset_id}", self._check_asset
538
+ )
539
+ app.router.add_post("/runtimes/{runtime_id}/rearrange", self._rearrange_runtime)
540
+ app.router.add_patch("/runtimes/{runtime_id}/state", self._patch_runtime_state)
541
+ app.router.add_post("/runtimes/{runtime_id}", self._process_runtime)
542
+
543
+ app.router.add_get("/runtimes/{runtime_id}/services", self._get_services)
544
+ app.router.add_post("/runtimes/{runtime_id}/services", self._post_service)
545
+ app.router.add_delete(
546
+ "/runtimes/{runtime_id}/services/{instance_id}", self._delete_service
547
+ )
548
+ app.router.add_post(
549
+ "/runtimes/{runtime_id}/services/{instance_id}", self._configure_service
550
+ )
551
+ app.router.add_post(
552
+ "/runtimes/{runtime_id}/services/{instance_id}/process",
553
+ self._process_service,
554
+ )
555
+ app.router.add_get(
556
+ "/runtimes/{runtime_id}/services/{instance_id}", self._get_service
557
+ )
558
+ app.router.add_get(
559
+ "/runtimes/{runtime_id}/services/{instance_id}/property/{property_id}",
560
+ self._get_service_property,
561
+ )
562
+
563
+ # WebSocket endpoint — matches /{runtimeId}
564
+ app.router.add_get("/{runtime_id}", self._websocket_handler)
565
+
566
+ return app
567
+
568
+ # ── URL helper ─────────────────────────────────────────────────────────────
569
+
570
+ def _public_mount_url(self, mount_path: str) -> str | None:
571
+ if not self._port:
572
+ return None
573
+ return f"http://{self._external_host}:{self._port}{mount_path}"
574
+
575
+ def _runtime_output_url(self, runtime_id: str) -> str:
576
+ return f"ws://{self._external_host}:{self._port}/{runtime_id}"
577
+
578
+ def _serialize_runtime(self, runtime: HostedRuntime) -> dict[str, Any]:
579
+ descriptor = runtime.serialize(self._runtime_output_url(runtime.id))
580
+ return _descriptor_to_dict(descriptor)
581
+
582
+ # ── Processing ─────────────────────────────────────────────────────────────
583
+
584
+ async def _process_off_loop(
585
+ self,
586
+ runtime: HostedRuntime,
587
+ body: Any,
588
+ context: ProcessContext | None = None,
589
+ ) -> Any:
590
+ """Run the pipeline in the worker thread so slow services (e.g. ML
591
+ inference) don't stall the event loop."""
592
+ return await asyncio.get_running_loop().run_in_executor(
593
+ self._process_executor, runtime.process, body, lambda _n: None, context
594
+ )
595
+
596
+ def _spawn(self, coro: Coroutine[Any, Any, Any]) -> None:
597
+ """Schedule a coroutine on the server loop; safe from worker threads."""
598
+ loop = self._loop
599
+ if loop is None:
600
+ coro.close()
601
+ return
602
+ try:
603
+ running = asyncio.get_running_loop()
604
+ except RuntimeError:
605
+ running = None
606
+ if running is loop:
607
+ asyncio.ensure_future(coro)
608
+ else:
609
+ asyncio.run_coroutine_threadsafe(coro, loop)
610
+
611
+ # ── Notification / result helpers ──────────────────────────────────────────
612
+
613
+ def _send_notification(
614
+ self, socket_key: str, notification: RuntimeNotification
615
+ ) -> None:
616
+ sockets = self._runtime_sockets.get(socket_key)
617
+ if not sockets:
618
+ return
619
+ message = json.dumps(
620
+ {
621
+ "type": "notification",
622
+ "instanceId": notification.instance_id,
623
+ "value": json.dumps(notification.payload, default=_json_placeholder),
624
+ }
625
+ )
626
+ for ws in list(sockets):
627
+ if not ws.closed:
628
+ self._spawn(ws.send_str(message))
629
+
630
+ def _send_log(self, socket_key: str, entry: LogEntry) -> None:
631
+ """Carry a log entry to whoever is collecting this runtime's output.
632
+
633
+ The same socket a notification takes, and for the same reason: it is the
634
+ connection the board's coordinator already holds, authenticated with a
635
+ credential minted to outlive the user's session. An entry differs in
636
+ what it is for — a notification is for whoever is watching, an entry has
637
+ to survive with nobody attached — but not in how it travels.
638
+
639
+ Nobody listening is not a reason to buffer: an entry exists to be
640
+ written down by the coordinator, and there is no coordinator here to
641
+ write it. A runtime nothing is collecting from is a runtime whose board
642
+ has no log.
643
+ """
644
+ sockets = self._runtime_sockets.get(socket_key)
645
+ if not sockets:
646
+ return
647
+ # `data` is whatever a service passed, which on this runtime may be a
648
+ # ring buffer or raw bytes — the same placeholder a notification uses,
649
+ # rather than failing to send the entry at all.
650
+ message = json.dumps(
651
+ {"type": "log", "entry": entry.to_wire()}, default=_json_placeholder
652
+ )
653
+ for ws in list(sockets):
654
+ if not ws.closed:
655
+ self._spawn(ws.send_str(message))
656
+
657
+ def _send_result(self, socket_key: str, result: Any) -> None:
658
+ sockets = self._runtime_sockets.get(socket_key)
659
+ if not sockets:
660
+ return
661
+ if _is_binary_result(result):
662
+ frame = serialize_message(result, purpose=MessagePurpose.RESULT)
663
+ for ws in list(sockets):
664
+ if not ws.closed:
665
+ self._spawn(ws.send_bytes(frame))
666
+ return
667
+ message = json.dumps({"type": "result", "data": _jsonable_result(result)})
668
+ for ws in list(sockets):
669
+ if not ws.closed:
670
+ self._spawn(ws.send_str(message))
671
+
672
+ def _register_runtime_targets(
673
+ self, owner: str, runtime: HostedRuntime, board_name: str | None
674
+ ) -> None:
675
+ """Wires what a runtime says to whoever is listening: the sockets
676
+ watching it, or the coordinator it was built for."""
677
+ runtime_id = runtime.id
678
+
679
+ if board_name is not None:
680
+ links = self.coordinator_links
681
+ linked = (owner, board_name, runtime_id)
682
+
683
+ def to_coordinator_notification(notification: RuntimeNotification) -> None:
684
+ message: dict[str, Any] = {
685
+ "type": "notification",
686
+ "serviceUuid": notification.instance_id,
687
+ # A coordinator always listens: what it is sent of a call is
688
+ # what Capture mode allows, as for any other listener.
689
+ "payload": outgoing_payload(notification.payload, runtime.captures_calls()),
690
+ }
691
+ # Carries the run captured at the service boundary, so the
692
+ # coordinator can address and revalidate it. A standing timer
693
+ # or subscription instead has its own board-origin run.
694
+ wire = context_to_wire(notification.context)
695
+ if wire is not None:
696
+ message["context"] = wire
697
+ links.emit(*linked, message)
698
+
699
+ def to_coordinator_result(
700
+ result: Any, context: ProcessContext | None
701
+ ) -> None:
702
+ # The link sends a value holding bytes as a binary frame. With
703
+ # the run it was emitted in, so the coordinator can tell the
704
+ # next runtime which run this continues and who began it.
705
+ links.emit(*linked, _result_message(result, context))
706
+
707
+ def to_coordinator_log(entry: LogEntry) -> None:
708
+ links.emit(*linked, {"type": "log", "entry": entry.to_wire()})
709
+
710
+ runtime.register_notification_target(to_coordinator_notification)
711
+ runtime.register_result_target(to_coordinator_result)
712
+ runtime.register_log_target(to_coordinator_log)
713
+ return
714
+
715
+ socket_key = _tenant_key(owner, runtime_id)
716
+ runtime.register_notification_target(
717
+ lambda notification: self._send_notification(
718
+ socket_key,
719
+ replace(
720
+ notification,
721
+ payload=outgoing_payload(notification.payload, runtime.captures_calls()),
722
+ ),
723
+ )
724
+ )
725
+ runtime.register_result_target(
726
+ lambda result, _context: self._send_result(socket_key, result)
727
+ )
728
+ runtime.register_log_target(lambda entry: self._send_log(socket_key, entry))
729
+
730
+ def _provision_runtime(
731
+ self,
732
+ owner: str,
733
+ config: RuntimeConfiguration,
734
+ board_name: str | None = None,
735
+ ) -> HostedRuntime:
736
+ """Builds a runtime for a tenant, replacing anything under that id.
737
+
738
+ A runtime a client asked for lives in the tenant's own space and speaks
739
+ to the sockets watching it. One built for a coordinator (``board_name``
740
+ given) lives in its board's space and speaks to that coordinator; see
741
+ ``board_space``.
742
+
743
+ Quotas apply only to genuinely new runtimes — re-creating one that
744
+ already exists must not be refused for being over the limit.
745
+ """
746
+ space = owner if board_name is None else board_space(owner, board_name)
747
+ is_new = self.runtime_app.get_runtime(space, config.id) is None
748
+ if is_new and self._at_quota(
749
+ self.runtime_app.count_runtimes(owner), self._max_runtimes_per_user
750
+ ):
751
+ raise _QuotaError(f"Runtime limit reached ({self._max_runtimes_per_user})")
752
+ if self._exceeds_quota(len(config.services), self._max_services_per_runtime):
753
+ raise _QuotaError(
754
+ f"Service limit reached ({self._max_services_per_runtime})"
755
+ )
756
+ runtime = self.runtime_app.create_runtime(owner, config, space)
757
+ self._register_runtime_targets(owner, runtime, board_name)
758
+ return runtime
759
+
760
+ def _linked_runtime(
761
+ self, owner: str, board_name: str, runtime_id: str
762
+ ) -> HostedRuntime | None:
763
+ return self.runtime_app.get_runtime(board_space(owner, board_name), runtime_id)
764
+
765
+ def _remove_runtime(self, owner: str, runtime_id: str) -> None:
766
+ self.runtime_app.remove_runtime(owner, runtime_id)
767
+ self._mounts.release_runtime(owner, runtime_id)
768
+ self._purge_session_tokens(owner, runtime_id)
769
+
770
+ @staticmethod
771
+ def _apply_runtime_state(
772
+ runtime: HostedRuntime, state: dict[str, Any]
773
+ ) -> dict[str, Any]:
774
+ """Applies the parts of a runtime's state that can change while it runs."""
775
+ if isinstance(state.get("logging"), bool):
776
+ runtime.set_logging(state["logging"])
777
+ if state.get("logLevel") in LOG_LEVELS:
778
+ runtime.set_log_level(state["logLevel"])
779
+ if isinstance(state.get("logData"), bool):
780
+ runtime.set_log_data(state["logData"])
781
+ if isinstance(state.get("capture"), bool):
782
+ runtime.set_capture(state["capture"])
783
+ settings = runtime.log_settings()
784
+ return {
785
+ "logging": settings["logging"],
786
+ "logData": settings["log_data"],
787
+ "logLevel": settings["log_level"],
788
+ # Answered, never configured: nothing that saves a runtime's
789
+ # configuration can keep it.
790
+ "capture": runtime.captures_calls(),
791
+ }
792
+
793
+ # ── What a coordinator may do here, over a link this server opened ─────────
794
+ #
795
+ # The operations on one runtime, as the tenant who introduced the link — the
796
+ # same things the REST routes do for a caller holding a token. See
797
+ # coordinator_links.LinkHost.
798
+
799
+ def link_registry(self) -> list[Any]:
800
+ return self.runtime_app.get_registry()
801
+
802
+ def link_runtime_exists(self, owner: str, board_name: str, runtime_id: str) -> bool:
803
+ return self._linked_runtime(owner, board_name, runtime_id) is not None
804
+
805
+ def link_provision(
806
+ self,
807
+ owner: str,
808
+ board_name: str,
809
+ runtime_id: str,
810
+ payload: dict[str, Any],
811
+ secrets: dict[str, SecretEntry],
812
+ ) -> dict[str, Any]:
813
+ config = _validate_runtime_configuration(
814
+ {
815
+ "id": runtime_id,
816
+ "name": payload.get("name"),
817
+ "boardName": board_name,
818
+ # The coordinator's until it says otherwise: a deployed board
819
+ # keeps running with nobody watching.
820
+ "garbageCollected": False,
821
+ "state": payload.get("state"),
822
+ "services": payload.get("services"),
823
+ # The board's assets for this runtime, as the coordinator sends
824
+ # them: descriptors, by id.
825
+ "assets": payload.get("assets"),
826
+ }
827
+ )
828
+ if config is None:
829
+ raise ValueError("The board's description of this runtime is malformed")
830
+ # The values this server was handed for the runtime, by the person's own
831
+ # client. They do not come from the coordinator and never go to it.
832
+ config.secrets = dict(secrets)
833
+ runtime = self._provision_runtime(owner, config, board_name)
834
+ held = set(runtime.secrets().aliases())
835
+ missing = [
836
+ alias
837
+ for alias in referenced_secrets(
838
+ [service.state for service in config.services]
839
+ )
840
+ if alias not in held
841
+ ]
842
+ return {
843
+ "registry": self.runtime_app.get_registry(),
844
+ "services": [
845
+ _service_descriptor_to_dict(s) for s in runtime.list_services()
846
+ ],
847
+ "missingSecrets": missing,
848
+ }
849
+
850
+ def link_describe(self, owner: str, board_name: str, runtime_id: str) -> dict[str, Any] | None:
851
+ runtime = self._linked_runtime(owner, board_name, runtime_id)
852
+ if runtime is None:
853
+ return None
854
+ return {
855
+ "services": [
856
+ _service_descriptor_to_dict(s) for s in runtime.list_services()
857
+ ]
858
+ }
859
+
860
+ async def link_configure_service(
861
+ self,
862
+ owner: str,
863
+ board_name: str,
864
+ runtime_id: str,
865
+ service_uuid: str,
866
+ config: Any,
867
+ context: Any,
868
+ ) -> Any:
869
+ runtime = self._linked_runtime(owner, board_name, runtime_id)
870
+ if runtime is None:
871
+ raise RuntimeError("the runtime is not running")
872
+ if not isinstance(config, dict):
873
+ raise ValueError("a service is configured with an object")
874
+ run = context_from_link(context) or new_run()
875
+ if runtime.configure_service(service_uuid, config, run) is None:
876
+ raise LookupError(f'no service "{service_uuid}"')
877
+ return await _wait_for_service_activation_state(runtime, service_uuid)
878
+
879
+ def link_set_state(
880
+ self, owner: str, board_name: str, runtime_id: str, state: dict[str, Any]
881
+ ) -> Any:
882
+ runtime = self._linked_runtime(owner, board_name, runtime_id)
883
+ if runtime is None:
884
+ raise RuntimeError("the runtime is not running")
885
+ return self._apply_runtime_state(runtime, state)
886
+
887
+ def link_remove(self, owner: str, board_name: str, runtime_id: str) -> None:
888
+ space = board_space(owner, board_name)
889
+ self.runtime_app.remove_runtime(space, runtime_id)
890
+ self._mounts.release_runtime(space, runtime_id)
891
+
892
+ async def link_process(
893
+ self, owner: str, board_name: str, runtime_id: str, params: Any, context: Any
894
+ ) -> dict[str, Any]:
895
+ runtime = self._linked_runtime(owner, board_name, runtime_id)
896
+ if runtime is None:
897
+ raise RuntimeError("the runtime is not running")
898
+ # The coordinator names the run its call belongs to, so that a board
899
+ # spanning several runtimes reads as one trace — and who began it,
900
+ # which is taken as stated on this path and on no other.
901
+ run = context_from_link(context) or new_run()
902
+ result = await self._process_off_loop(runtime, params, run)
903
+ return _result_message(result, run)
904
+
905
+ def link_process_service(
906
+ self,
907
+ owner: str,
908
+ board_name: str,
909
+ runtime_id: str,
910
+ service_uuid: str,
911
+ params: Any,
912
+ context: Any,
913
+ done: Callable[[dict[str, Any]], None],
914
+ ) -> None:
915
+ runtime = self._linked_runtime(owner, board_name, runtime_id)
916
+ if runtime is None:
917
+ raise RuntimeError("the runtime is not running")
918
+ if not runtime.get_service(service_uuid):
919
+ raise RuntimeError(f'no service "{service_uuid}"')
920
+ # As on `link_process`: the run and its caller are the coordinator's
921
+ # to state, over this link and nowhere else.
922
+ run = context_from_link(context) or new_run()
923
+
924
+ async def work() -> None:
925
+ try:
926
+ result = await asyncio.get_running_loop().run_in_executor(
927
+ self._process_executor,
928
+ runtime.process_at,
929
+ service_uuid,
930
+ params,
931
+ lambda _n: None,
932
+ run,
933
+ )
934
+ except Exception as err: # noqa: BLE001 - reported, never raised
935
+ print(
936
+ f'[coordinator-link] Runtime "{runtime_id}" failed to '
937
+ f'process at "{service_uuid}": {err}'
938
+ )
939
+ return
940
+ done(_result_message(result, run))
941
+
942
+ self._spawn(work())
943
+
944
+ # ── /coordinator-links handlers ────────────────────────────────────────────
945
+
946
+ async def _post_coordinator_link(self, request: web.Request) -> web.Response:
947
+ """Introduces this server to a coordinator, for one runtime of one board.
948
+
949
+ Called by the person's own client while it deploys a board: it has asked
950
+ the coordinator for a ticket and passes it on, over the same session it
951
+ creates runtimes here with. This server then connects to the coordinator
952
+ — the coordinator connects to nothing — and keeps the ticket to
953
+ reconnect with.
954
+
955
+ The address dialled is one the caller chose, and the caller is someone
956
+ this server already runs services for; nothing here can be made to reach
957
+ further than they could with a service of their own.
958
+
959
+ ``secrets`` are the values for the references that runtime's services
960
+ carry. They are handed to the runtime when the coordinator builds it,
961
+ and are not sent to the coordinator.
962
+ """
963
+ try:
964
+ body = await request.json()
965
+ except Exception:
966
+ raise web.HTTPBadRequest()
967
+ fields = ("coordinatorUrl", "ticket", "boardName", "runtimeId")
968
+ if not isinstance(body, dict) or not all(
969
+ isinstance(body.get(name), str) and body[name] for name in fields
970
+ ):
971
+ raise web.HTTPBadRequest()
972
+ try:
973
+ await self.coordinator_links.introduce(
974
+ LinkRecord(
975
+ owner=self._owner_of(request),
976
+ board_name=body["boardName"],
977
+ runtime_id=body["runtimeId"],
978
+ coordinator_url=body["coordinatorUrl"],
979
+ ticket=body["ticket"],
980
+ ),
981
+ read_secrets_payload(body.get("secrets")),
982
+ )
983
+ except (ConnectionError, ValueError) as err:
984
+ return web.json_response(
985
+ {"error": str(err) or "Could not connect"}, status=502
986
+ )
987
+ return web.json_response({"connected": True}, status=201)
988
+
989
+ async def _get_coordinator_links(self, request: web.Request) -> web.Response:
990
+ """The caller's links: which runtimes belong to which board, never a
991
+ ticket."""
992
+ return web.json_response(
993
+ {"links": self.coordinator_links.list(self._owner_of(request))}
994
+ )
995
+
996
+ async def _delete_coordinator_link(self, request: web.Request) -> web.Response:
997
+ """Leaves a board: drops the link and the runtime it was for."""
998
+ removed = await self.coordinator_links.remove(
999
+ self._owner_of(request),
1000
+ request.match_info["board_name"],
1001
+ request.match_info["runtime_id"],
1002
+ )
1003
+ return web.Response(status=200 if removed else 404)
1004
+
1005
+ # ── /runtimes handlers ─────────────────────────────────────────────────────
1006
+
1007
+ async def _get_runtimes(self, request: web.Request) -> web.Response:
1008
+ return web.json_response(
1009
+ {
1010
+ "runtimes": [
1011
+ self._serialize_runtime(rt)
1012
+ for rt in self._tenant_of(request).get_runtimes()
1013
+ ],
1014
+ # The service registry is a property of the build, not a tenant.
1015
+ "registry": self.runtime_app.get_registry(),
1016
+ "server": RUNTIME_SERVER_KIND,
1017
+ # This server can connect to a coordinator when introduced to
1018
+ # one; see POST /coordinator-links. Said here so a client can
1019
+ # tell before it deploys a board that needs it.
1020
+ "coordinatorLinks": True,
1021
+ # A runtime built for a coordinator is kept apart from the ones
1022
+ # a client creates, so a client deleting its own does not
1023
+ # delete a deployed board's. A client checks for this before it
1024
+ # deploys: against a server that keeps them together, leaving
1025
+ # the board it deployed would stop it.
1026
+ "boardRuntimes": True,
1027
+ }
1028
+ )
1029
+
1030
+ async def _post_ws_ticket(self, request: web.Request) -> web.Response:
1031
+ """A ticket for a WebSocket, minted for whoever this request's token
1032
+ let in, so the socket is opened without the token in its address."""
1033
+ return web.json_response(
1034
+ {
1035
+ "ticket": self._ws_tickets.mint(request[_AUTHENTICATED_USER_KEY]),
1036
+ "expiresInMs": int(TICKET_TTL_SECONDS * 1000),
1037
+ }
1038
+ )
1039
+
1040
+ async def _post_runtimes(self, request: web.Request) -> web.Response:
1041
+ try:
1042
+ body = await request.json()
1043
+ except Exception:
1044
+ raise web.HTTPBadRequest()
1045
+
1046
+ owner = self._owner_of(request)
1047
+ payloads = body if isinstance(body, list) else [body]
1048
+ runtimes = []
1049
+
1050
+ for payload in payloads:
1051
+ config = _validate_runtime_configuration(payload)
1052
+ if config is None:
1053
+ raise web.HTTPBadRequest()
1054
+
1055
+ try:
1056
+ runtime = self._provision_runtime(owner, config)
1057
+ except _QuotaError as err:
1058
+ raise web.HTTPTooManyRequests(
1059
+ text=json.dumps({"error": str(err)}),
1060
+ content_type="application/json",
1061
+ )
1062
+ runtimes.append(self._serialize_runtime(runtime))
1063
+
1064
+ return web.json_response(
1065
+ {
1066
+ "runtimes": runtimes,
1067
+ "registry": self.runtime_app.get_registry(),
1068
+ "server": RUNTIME_SERVER_KIND,
1069
+ }
1070
+ )
1071
+
1072
+ async def _delete_runtimes(self, request: web.Request) -> web.Response:
1073
+ owner = self._owner_of(request)
1074
+ self.runtime_app.remove_all_runtimes(owner)
1075
+ self._mounts.release_owner(owner)
1076
+ self._purge_owner_session_tokens(owner)
1077
+ return web.Response(status=200)
1078
+
1079
+ # ── /runtimes/{id} handlers ────────────────────────────────────────────────
1080
+
1081
+ async def _get_runtime(self, request: web.Request) -> web.Response:
1082
+ runtime = self._get_runtime_or_404(request)
1083
+ return web.json_response(self._serialize_runtime(runtime))
1084
+
1085
+ async def _delete_runtime(self, request: web.Request) -> web.Response:
1086
+ runtime_id = request.match_info["runtime_id"]
1087
+ owner = self._owner_of(request)
1088
+ # Idempotent: a runtime already gone is the desired end state, not an
1089
+ # error. A client removing a runtime closes its notification socket
1090
+ # first, and that close reaps a garbage-collected runtime on its own —
1091
+ # so by the time this explicit DELETE arrives the runtime is frequently
1092
+ # already removed. Reporting NOT_FOUND there surfaces a spurious
1093
+ # "Failed to remove runtime" to the user.
1094
+ #
1095
+ # Scoped to the caller, so this can only ever remove their own runtime;
1096
+ # an id owned by another tenant is indistinguishable from a missing one.
1097
+ self.runtime_app.remove_runtime(owner, runtime_id)
1098
+ self._mounts.release_runtime(owner, runtime_id)
1099
+ self._purge_session_tokens(owner, runtime_id)
1100
+ return web.json_response({"id": runtime_id})
1101
+
1102
+ def _reap_if_abandoned(self, owner: str, runtime_id: str) -> None:
1103
+ """Tear down a runtime whose creator asked for cleanup, now that its
1104
+ last client has disconnected.
1105
+
1106
+ Whoever created it said whether it should outlive its clients. A browser
1107
+ running a board is that board's controller and asks for cleanup: closing
1108
+ the tab should not leave runtimes behind. A coordinator, a config file
1109
+ or a script says nothing, and their runtimes stay until deleted — a
1110
+ headless runtime is not an abandoned one, and a runtime is never reaped
1111
+ because of who happened to connect to it.
1112
+ """
1113
+ runtime = self.runtime_app.get_runtime(owner, runtime_id)
1114
+ if not runtime or not runtime.garbage_collected:
1115
+ return
1116
+ self.runtime_app.remove_runtime(owner, runtime_id)
1117
+ self._mounts.release_runtime(owner, runtime_id)
1118
+ self._purge_session_tokens(owner, runtime_id)
1119
+
1120
+ async def _mint_session_token(self, request: web.Request) -> web.Response:
1121
+ """Mint a coordinator session token for a runtime. Gated by the normal
1122
+ auth middleware, so the caller must present a valid user JWT (the
1123
+ "bootstrap"). The returned opaque token is bound to that user and this
1124
+ runtime, and the coordinator then uses it for its long-lived machine
1125
+ calls without needing a user JWT that would expire.
1126
+
1127
+ Limitation (v1, same as hkp-node): tokens live only in this process. If
1128
+ the runtime restarts the token is gone and the coordinator must
1129
+ re-provision — which requires a live user JWT.
1130
+ """
1131
+ runtime = self._get_runtime_or_404(request)
1132
+ sub = self._owner_of(request)
1133
+ token = secrets.token_hex(32)
1134
+ self._session_tokens[token] = SessionToken(sub=sub, runtime_id=runtime.id)
1135
+ return web.json_response({"token": token})
1136
+
1137
+ async def _post_secrets(self, request: web.Request) -> web.Response:
1138
+ """Values for the references this runtime's services hold.
1139
+
1140
+ Provisioning carries them already; this is for the moments it cannot
1141
+ cover — a board being built a service at a time, an entry edited while a
1142
+ board is running, and a re-push after a restart where the services
1143
+ survived but the vault did not. It merges, so a client sending one entry
1144
+ does not strip the rest.
1145
+
1146
+ POST rather than PUT: it merges rather than replaces, and every other
1147
+ mutation this server takes is a POST — the CORS allowlist says so, and a
1148
+ lone PUT is a method each runtime implementation would have to remember
1149
+ to allow separately.
1150
+
1151
+ There is deliberately no GET. The values go one way: in, and then only
1152
+ to a service resolving a reference for a call it is making. What is held
1153
+ can be *named* — the response says which aliases the runtime now has —
1154
+ because a client needs to show whether a credential is configured.
1155
+ """
1156
+ runtime = self._get_runtime_or_404(request)
1157
+ try:
1158
+ body = await request.json()
1159
+ except Exception:
1160
+ raise web.HTTPBadRequest()
1161
+ if not isinstance(body, dict):
1162
+ raise web.HTTPBadRequest()
1163
+ runtime.set_secrets(read_secrets_payload(body))
1164
+ return web.json_response({"aliases": runtime.secrets().aliases()})
1165
+
1166
+ async def _post_assets(self, request: web.Request) -> web.Response:
1167
+ """Descriptors for the assets this runtime's services reference.
1168
+
1169
+ Provisioning carries them already; this is for a configuration naming
1170
+ one the runtime was not given, for an asset edited while the board runs
1171
+ — which is how an edit reaches a service without reconfiguring it — and
1172
+ for a re-push after a restart. It merges, and ``null`` removes an asset.
1173
+ Answers with the ids held, never content.
1174
+ """
1175
+ runtime = self._get_runtime_or_404(request)
1176
+ try:
1177
+ body = await request.json()
1178
+ except Exception:
1179
+ raise web.HTTPBadRequest()
1180
+ if not isinstance(body, (dict, list)):
1181
+ raise web.HTTPBadRequest()
1182
+ runtime.set_assets(read_assets_payload(body))
1183
+ return web.json_response({"ids": runtime.assets().ids()})
1184
+
1185
+ async def _check_asset(self, request: web.Request) -> web.Response:
1186
+ """Whether an asset resolves here, and to what — a check, not a download."""
1187
+ runtime = self._get_runtime_or_404(request)
1188
+ resolution = await asyncio.to_thread(
1189
+ runtime.assets().resolve, f"hkp-asset://{request.match_info['asset_id']}"
1190
+ )
1191
+ if resolution.asset is None:
1192
+ return web.json_response({"ok": False, "problem": resolution.problem})
1193
+ return web.json_response(
1194
+ {
1195
+ "ok": True,
1196
+ "mediaType": resolution.asset.media_type,
1197
+ "size": len(resolution.asset.content),
1198
+ }
1199
+ )
1200
+
1201
+ async def _rearrange_runtime(self, request: web.Request) -> web.Response:
1202
+ runtime = self._get_runtime_or_404(request)
1203
+ try:
1204
+ body = await request.json()
1205
+ except Exception:
1206
+ raise web.HTTPBadRequest()
1207
+ if not isinstance(body, list) or not all(isinstance(e, str) for e in body):
1208
+ raise web.HTTPBadRequest()
1209
+ if not runtime.rearrange_services(body):
1210
+ raise web.HTTPBadRequest()
1211
+ return web.json_response(self._serialize_runtime(runtime))
1212
+
1213
+ async def _patch_runtime_state(self, request: web.Request) -> web.Response:
1214
+ """Change what a running runtime records, without rebuilding it.
1215
+
1216
+ Logging is a decision a board revisits — switched on to look into
1217
+ something, off again afterwards — and re-provisioning to carry it would
1218
+ restart every service in the runtime to change one boolean. That is what
1219
+ the coordinator's per-board log switch calls, and a runtime that does
1220
+ not answer it is reported as one the switch did not reach.
1221
+
1222
+ Separate from POST /runtimes/{id}, which processes data rather than
1223
+ configuring anything.
1224
+ """
1225
+ runtime = self._get_runtime_or_404(request)
1226
+ try:
1227
+ body = await request.json()
1228
+ except Exception:
1229
+ raise web.HTTPBadRequest()
1230
+ if not isinstance(body, dict):
1231
+ raise web.HTTPBadRequest()
1232
+ return web.json_response(self._apply_runtime_state(runtime, body))
1233
+
1234
+ async def _process_runtime(self, request: web.Request) -> web.Response:
1235
+ runtime = self._get_runtime_or_404(request)
1236
+ raw = await request.read()
1237
+
1238
+ if request.content_type == "application/octet-stream" or is_yas_message(raw):
1239
+ try:
1240
+ body = deserialize_message(raw).data
1241
+ except YasError:
1242
+ raise web.HTTPBadRequest()
1243
+ else:
1244
+ try:
1245
+ body = json.loads(raw)
1246
+ except Exception:
1247
+ raise web.HTTPBadRequest()
1248
+ # `null` is a payload: it says run with nothing on the input, which
1249
+ # is how a caller writes "no input" in a format that has no
1250
+ # undefined. The websocket path already accepts it, and a service
1251
+ # that answers an empty input with its own configuration needs it.
1252
+ if body is not None and not isinstance(body, dict):
1253
+ raise web.HTTPBadRequest()
1254
+
1255
+ # An external HTTP caller is not continuing a run, it is starting one —
1256
+ # as whoever its token says it is.
1257
+ result = await self._process_off_loop(
1258
+ runtime,
1259
+ body,
1260
+ context_for_client(None, request.get(_AUTHENTICATED_USER_KEY)),
1261
+ )
1262
+ if _is_binary_result(result):
1263
+ return web.Response(
1264
+ body=serialize_message(result, purpose=MessagePurpose.RESULT),
1265
+ content_type="application/octet-stream",
1266
+ )
1267
+ return web.json_response(_jsonable_result(result))
1268
+
1269
+ async def _process_service(self, request: web.Request) -> web.Response:
1270
+ """Run the pipeline starting at one service, with a given payload.
1271
+
1272
+ Distinct from configuring it: configure says what a service *is*, this
1273
+ says do your job with this. A facade button had only the former, so
1274
+ anything it needed to cause had to be smuggled in as a config field that
1275
+ a service read as a command.
1276
+ """
1277
+ runtime = self._get_runtime_or_404(request)
1278
+ instance_id = request.match_info["instance_id"]
1279
+ raw = await request.read()
1280
+
1281
+ if request.content_type == "application/octet-stream" or is_yas_message(raw):
1282
+ try:
1283
+ body = deserialize_message(raw).data
1284
+ except YasError:
1285
+ raise web.HTTPBadRequest()
1286
+ else:
1287
+ try:
1288
+ body = json.loads(raw) if raw else {}
1289
+ except Exception:
1290
+ raise web.HTTPBadRequest()
1291
+
1292
+ try:
1293
+ result = await asyncio.get_running_loop().run_in_executor(
1294
+ self._process_executor,
1295
+ runtime.process_at,
1296
+ instance_id,
1297
+ body,
1298
+ lambda _n: None,
1299
+ # The caller is never the body's to name: it is whoever the
1300
+ # token was verified as.
1301
+ context_for_client(
1302
+ body.get("__context") if isinstance(body, dict) else None,
1303
+ request.get(_AUTHENTICATED_USER_KEY),
1304
+ ),
1305
+ )
1306
+ except KeyError:
1307
+ raise web.HTTPNotFound()
1308
+
1309
+ if _is_binary_result(result):
1310
+ return web.Response(
1311
+ body=serialize_message(result, purpose=MessagePurpose.RESULT),
1312
+ content_type="application/octet-stream",
1313
+ )
1314
+ return web.json_response(_jsonable_result(result))
1315
+
1316
+ # ── /runtimes/{id}/services handlers ──────────────────────────────────────
1317
+
1318
+ async def _get_services(self, request: web.Request) -> web.Response:
1319
+ runtime = self._get_runtime_or_404(request)
1320
+ return web.json_response(
1321
+ [_service_descriptor_to_dict(s) for s in runtime.list_services()]
1322
+ )
1323
+
1324
+ async def _post_service(self, request: web.Request) -> web.Response:
1325
+ runtime = self._get_runtime_or_404(request)
1326
+ try:
1327
+ body = await request.json()
1328
+ except Exception:
1329
+ raise web.HTTPBadRequest()
1330
+ config = _validate_service_configuration(body)
1331
+ if config is None:
1332
+ raise web.HTTPBadRequest()
1333
+ if self._at_quota(
1334
+ len(runtime.list_services()), self._max_services_per_runtime
1335
+ ):
1336
+ raise web.HTTPTooManyRequests(
1337
+ text=json.dumps(
1338
+ {
1339
+ "error": f"Service limit reached ({self._max_services_per_runtime})"
1340
+ }
1341
+ ),
1342
+ content_type="application/json",
1343
+ )
1344
+ try:
1345
+ state = runtime.add_service(config)
1346
+ except Exception:
1347
+ raise web.HTTPBadRequest()
1348
+ return web.json_response(state)
1349
+
1350
+ async def _delete_service(self, request: web.Request) -> web.Response:
1351
+ runtime = self._get_runtime_or_404(request)
1352
+ instance_id = request.match_info["instance_id"]
1353
+ if not runtime.remove_service(instance_id):
1354
+ raise web.HTTPNotFound()
1355
+ return web.json_response(self._serialize_runtime(runtime))
1356
+
1357
+ async def _configure_service(self, request: web.Request) -> web.Response:
1358
+ runtime = self._get_runtime_or_404(request)
1359
+ instance_id = request.match_info["instance_id"]
1360
+ try:
1361
+ body = await request.json()
1362
+ except Exception:
1363
+ raise web.HTTPBadRequest()
1364
+ if not isinstance(body, dict):
1365
+ raise web.HTTPBadRequest()
1366
+ state = runtime.configure_service(
1367
+ instance_id,
1368
+ body,
1369
+ context_for_client(None, request.get(_AUTHENTICATED_USER_KEY)),
1370
+ )
1371
+ if state is None:
1372
+ raise web.HTTPNotFound()
1373
+ # Poll until port is assigned for http-server-subservices with bypass=False, port=0
1374
+ state = await _wait_for_service_activation_state(runtime, instance_id)
1375
+ return web.json_response(state)
1376
+
1377
+ async def _get_service(self, request: web.Request) -> web.Response:
1378
+ runtime = self._get_runtime_or_404(request)
1379
+ instance_id = request.match_info["instance_id"]
1380
+ svc = runtime.get_service(instance_id)
1381
+ if not svc:
1382
+ raise web.HTTPNotFound()
1383
+ return web.json_response(svc.get_state())
1384
+
1385
+ async def _get_service_property(self, request: web.Request) -> web.Response:
1386
+ runtime = self._get_runtime_or_404(request)
1387
+ instance_id = request.match_info["instance_id"]
1388
+ property_id = request.match_info["property_id"]
1389
+ svc = runtime.get_service(instance_id)
1390
+ if not svc:
1391
+ raise web.HTTPNotFound()
1392
+ state = svc.get_state()
1393
+ if property_id not in state:
1394
+ raise web.HTTPNotFound()
1395
+ return web.json_response(state[property_id])
1396
+
1397
+ # ── WebSocket handler ──────────────────────────────────────────────────────
1398
+
1399
+ async def _websocket_handler(self, request: web.Request) -> web.WebSocketResponse:
1400
+ runtime_id = request.match_info["runtime_id"]
1401
+ # The runtime is resolved in the authenticated user's namespace, so a
1402
+ # token cannot open a socket onto another tenant's runtime; an id owned
1403
+ # by someone else is indistinguishable from one that does not exist.
1404
+ owner = self._owner_of(request)
1405
+ if not self.runtime_app.get_runtime(owner, runtime_id):
1406
+ raise web.HTTPNotFound()
1407
+
1408
+ ws = web.WebSocketResponse()
1409
+ await ws.prepare(request)
1410
+
1411
+ socket_key = _tenant_key(owner, runtime_id)
1412
+ sockets = self._runtime_sockets.setdefault(socket_key, set())
1413
+ sockets.add(ws)
1414
+
1415
+ try:
1416
+ async for msg in ws:
1417
+ if msg.type == WSMsgType.TEXT:
1418
+ try:
1419
+ data = json.loads(msg.data)
1420
+ except json.JSONDecodeError:
1421
+ continue
1422
+
1423
+ if data.get("type") == "readwrite":
1424
+ continue # protocol handshake, nothing to do
1425
+
1426
+ # Mirror hkp-node: any JSON params value is processable —
1427
+ # a plain-text Injector ships a bare string, not an object.
1428
+ if data.get("type") == "processRuntime" and "params" in data:
1429
+ runtime = self.runtime_app.get_runtime(owner, runtime_id)
1430
+ if runtime:
1431
+ # A peer driving this runtime names the run its
1432
+ # call belongs to, so that a board spanning several
1433
+ # runtimes reads as one trace rather than one each.
1434
+ # Who is calling is not the frame's to say: it is
1435
+ # whoever opened this socket.
1436
+ result = await self._process_off_loop(
1437
+ runtime,
1438
+ data["params"],
1439
+ context_for_client(
1440
+ data.get("context"),
1441
+ request.get(_AUTHENTICATED_USER_KEY),
1442
+ ),
1443
+ )
1444
+ if not ws.closed:
1445
+ if _is_binary_result(result):
1446
+ await ws.send_bytes(
1447
+ serialize_message(
1448
+ result, purpose=MessagePurpose.RESULT
1449
+ )
1450
+ )
1451
+ else:
1452
+ await ws.send_str(
1453
+ json.dumps(
1454
+ {
1455
+ "type": "result",
1456
+ "data": _jsonable_result(result),
1457
+ }
1458
+ )
1459
+ )
1460
+ elif msg.type == WSMsgType.BINARY:
1461
+ # Binary frames are YAS-encoded data to process (the
1462
+ # frontend ships FloatRingBuffer etc. this way).
1463
+ try:
1464
+ message = deserialize_message(msg.data)
1465
+ except YasError:
1466
+ continue
1467
+ runtime = self.runtime_app.get_runtime(owner, runtime_id)
1468
+ if runtime:
1469
+ # Bytes have nowhere to name a run, so this begins one;
1470
+ # it is still whoever opened this socket that began it.
1471
+ result = await self._process_off_loop(
1472
+ runtime,
1473
+ message.data,
1474
+ context_for_client(
1475
+ None, request.get(_AUTHENTICATED_USER_KEY)
1476
+ ),
1477
+ )
1478
+ if not ws.closed:
1479
+ if _is_binary_result(result):
1480
+ await ws.send_bytes(
1481
+ serialize_message(
1482
+ result, purpose=MessagePurpose.RESULT
1483
+ )
1484
+ )
1485
+ else:
1486
+ await ws.send_str(
1487
+ json.dumps(
1488
+ {
1489
+ "type": "result",
1490
+ "data": _jsonable_result(result),
1491
+ }
1492
+ )
1493
+ )
1494
+ elif msg.type == WSMsgType.ERROR:
1495
+ break
1496
+ finally:
1497
+ sockets.discard(ws)
1498
+ if not sockets:
1499
+ self._runtime_sockets.pop(socket_key, None)
1500
+ self._reap_if_abandoned(owner, runtime_id)
1501
+
1502
+ return ws
1503
+
1504
+ # ── Utility ────────────────────────────────────────────────────────────────
1505
+
1506
+ def _get_runtime_or_404(self, request: web.Request) -> HostedRuntime:
1507
+ """Resolve a runtime inside the caller's namespace.
1508
+
1509
+ A runtime owned by another tenant is reported as 404 rather than 403, so
1510
+ runtime ids belonging to other users cannot be probed for existence.
1511
+ """
1512
+ runtime_id = request.match_info["runtime_id"]
1513
+ runtime = self._tenant_of(request).get_runtime(runtime_id)
1514
+ if not runtime:
1515
+ raise web.HTTPNotFound()
1516
+ return runtime
1517
+
1518
+
1519
+ # ── Module-level factory ───────────────────────────────────────────────────────
1520
+
1521
+
1522
+ def create_runtime_server(options: dict[str, Any] | None = None) -> RuntimeServer:
1523
+ return RuntimeServer(options or {})
1524
+
1525
+
1526
+ # ── Result / notification encoding helpers ─────────────────────────────────────
1527
+
1528
+
1529
+ def _is_binary_result(result: Any) -> bool:
1530
+ return isinstance(result, (FloatRingBuffer, BinaryData, TextData))
1531
+
1532
+
1533
+ def _jsonable_result(result: Any) -> Any:
1534
+ if isinstance(result, (NullData, UndefinedData)):
1535
+ return None
1536
+ return result
1537
+
1538
+
1539
+ def _result_message(result: Any, context: ProcessContext | None) -> dict[str, Any]:
1540
+ """What a runtime produced, as its coordinator is told: the value, and the
1541
+ run it was produced in — which the coordinator hands to the next runtime."""
1542
+ message: dict[str, Any] = {"type": "result", "data": _jsonable_result(result)}
1543
+ wire = context_to_wire(context)
1544
+ if wire is not None:
1545
+ message["context"] = wire
1546
+ return message
1547
+
1548
+
1549
+ def _json_placeholder(value: Any) -> Any:
1550
+ """json.dumps fallback so binary pipeline data can appear in notifications."""
1551
+ if isinstance(value, FloatRingBuffer):
1552
+ return {
1553
+ "type": "FloatRingBuffer",
1554
+ "numSamples": value.num_samples,
1555
+ "id": value.id,
1556
+ "ts": value.ts,
1557
+ }
1558
+ if isinstance(value, BinaryData):
1559
+ return {"type": "BinaryData", "size": len(value.data)}
1560
+ if isinstance(value, TextData):
1561
+ return value.text
1562
+ if isinstance(value, (NullData, UndefinedData)):
1563
+ return None
1564
+ return repr(value)
1565
+
1566
+
1567
+ # ── Middleware ─────────────────────────────────────────────────────────────────
1568
+
1569
+
1570
+ def _allow_origin(response: web.StreamResponse, origin: str | None) -> None:
1571
+ """Lets the page at ``origin`` read ``response``."""
1572
+ response.headers["Access-Control-Allow-Origin"] = origin or ""
1573
+ response.headers["Access-Control-Allow-Credentials"] = "true"
1574
+ response.headers["Vary"] = "Origin"
1575
+
1576
+
1577
+ @web.middleware
1578
+ async def _error_middleware(request: web.Request, handler: Any) -> web.Response:
1579
+ try:
1580
+ return await handler(request)
1581
+ except web.HTTPException:
1582
+ raise
1583
+ except Exception as exc:
1584
+ # Don't leak internal error details (paths, stack hints) to clients.
1585
+ print(f"[server] Unhandled request error: {exc!r}")
1586
+ return web.Response(
1587
+ status=500,
1588
+ content_type="application/json",
1589
+ text=json.dumps({"error": "Internal Server Error"}),
1590
+ )
1591
+
1592
+
1593
+ # ── Validation helpers ─────────────────────────────────────────────────────────
1594
+
1595
+
1596
+ def _validate_runtime_configuration(value: Any) -> RuntimeConfiguration | None:
1597
+ if not isinstance(value, dict):
1598
+ return None
1599
+ if not isinstance(value.get("id"), str) or not isinstance(value.get("name"), str):
1600
+ return None
1601
+ if not isinstance(value.get("services"), list):
1602
+ return None
1603
+
1604
+ services = []
1605
+ for entry in value["services"]:
1606
+ svc = _validate_service_configuration(entry)
1607
+ if svc is None:
1608
+ return None
1609
+ services.append(svc)
1610
+
1611
+ return RuntimeConfiguration(
1612
+ id=value["id"],
1613
+ name=value["name"],
1614
+ board_name=value.get("boardName", ""),
1615
+ # Absent means persist; see RuntimeConfiguration.garbage_collected.
1616
+ garbage_collected=value.get("garbageCollected") is True,
1617
+ # Both absent mean off; see RuntimeConfiguration.logging / log_data.
1618
+ logging=isinstance(value.get("state"), dict)
1619
+ and value["state"].get("logging") is True,
1620
+ log_level=(
1621
+ value["state"]["logLevel"]
1622
+ if isinstance(value.get("state"), dict)
1623
+ and value["state"].get("logLevel")
1624
+ in ("debug", "info", "warn", "error")
1625
+ else "info"
1626
+ ),
1627
+ # Absent means allowed; see RuntimeConfiguration.log_data.
1628
+ log_data=not (
1629
+ isinstance(value.get("state"), dict)
1630
+ and value["state"].get("logData") is False
1631
+ ),
1632
+ # Values for the references the services carry. Read out of the payload
1633
+ # here and handed to the runtime's vault; they are never put back into
1634
+ # any service's state, and never appear in a serialized runtime.
1635
+ secrets=read_secrets_payload(value.get("secrets")),
1636
+ # Descriptors for the assets the services reference; a removal means
1637
+ # nothing to a runtime being created, so only descriptors are kept.
1638
+ assets={
1639
+ asset_id: entry
1640
+ for asset_id, entry in read_assets_payload(value.get("assets")).items()
1641
+ if entry is not None
1642
+ },
1643
+ services=services,
1644
+ )
1645
+
1646
+
1647
+ def _validate_service_configuration(value: Any) -> ServiceConfiguration | None:
1648
+ if not isinstance(value, dict):
1649
+ return None
1650
+ if not isinstance(value.get("serviceId"), str) or not isinstance(value.get("uuid"), str):
1651
+ return None
1652
+ state = value.get("state")
1653
+ if state is not None and not isinstance(state, dict):
1654
+ return None
1655
+ return ServiceConfiguration(
1656
+ service_id=value["serviceId"],
1657
+ uuid=value["uuid"],
1658
+ name=value.get("name") if isinstance(value.get("name"), str) else None,
1659
+ service_name=value.get("serviceName") if isinstance(value.get("serviceName"), str) else None,
1660
+ state=state,
1661
+ )
1662
+
1663
+
1664
+ # ── Async polling helper ───────────────────────────────────────────────────────
1665
+
1666
+
1667
+ async def _wait_for_service_activation_state(
1668
+ runtime: HostedRuntime, instance_id: str
1669
+ ) -> JsonRecord:
1670
+ max_attempts = 20
1671
+ delay = 0.01
1672
+
1673
+ for _ in range(max_attempts):
1674
+ svc = runtime.get_service(instance_id)
1675
+ if not svc:
1676
+ return {}
1677
+ state = svc.get_state()
1678
+ if state.get("bypass") is False and state.get("port") == 0:
1679
+ await asyncio.sleep(delay)
1680
+ continue
1681
+ return state
1682
+
1683
+ svc = runtime.get_service(instance_id)
1684
+ return svc.get_state() if svc else {}
1685
+
1686
+
1687
+ # ── Serialisation helpers ──────────────────────────────────────────────────────
1688
+
1689
+
1690
+ def _descriptor_to_dict(descriptor: Any) -> dict[str, Any]:
1691
+ result: dict[str, Any] = {
1692
+ "id": descriptor.id,
1693
+ "name": descriptor.name,
1694
+ "boardName": descriptor.board_name,
1695
+ "services": [_service_descriptor_to_dict(s) for s in descriptor.services],
1696
+ "inputs": descriptor.inputs,
1697
+ }
1698
+ if descriptor.output_url is not None:
1699
+ result["outputUrl"] = descriptor.output_url
1700
+ return result
1701
+
1702
+
1703
+ def _service_descriptor_to_dict(descriptor: Any) -> dict[str, Any]:
1704
+ result: dict[str, Any] = {
1705
+ "serviceId": descriptor.service_id,
1706
+ "serviceName": descriptor.service_name,
1707
+ "uuid": descriptor.uuid,
1708
+ "state": descriptor.state,
1709
+ }
1710
+ if descriptor.version is not None:
1711
+ result["version"] = descriptor.version
1712
+ if descriptor.capabilities is not None:
1713
+ result["capabilities"] = descriptor.capabilities
1714
+ return result