@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.
- package/README.md +114 -2
- package/bundle/LICENSE +201 -0
- package/bundle/boards/aggregator-demo-board.json +54 -0
- package/bundle/boards/alpaca-board.json +316 -0
- package/bundle/boards/animate-board.json +159 -0
- package/bundle/boards/ascii-art-demo-board.json +33 -0
- package/bundle/boards/ascii-cam-board.json +184 -0
- package/bundle/boards/asset-demo-board.json +54 -0
- package/bundle/boards/audio-encode-demo-board.json +153 -0
- package/bundle/boards/audiolize-board.json +152 -0
- package/bundle/boards/batcher-demo-board.json +43 -0
- package/bundle/boards/buffer-demo-board.json +53 -0
- package/bundle/boards/cache-demo-board.json +59 -0
- package/bundle/boards/camera-demo-board.json +28 -0
- package/bundle/boards/canvas-demo-board.json +55 -0
- package/bundle/boards/changes-demo-board.json +60 -0
- package/bundle/boards/court-booking-browser-demo-board.json +221 -0
- package/bundle/boards/court-booking-demo-board.json +218 -0
- package/bundle/boards/current-time-board.json +32 -0
- package/bundle/boards/debounce-demo-board.json +43 -0
- package/bundle/boards/decrypt-demo-board.json +47 -0
- package/bundle/boards/delay-demo-board.json +28 -0
- package/bundle/boards/detect-demo-board.json +64 -0
- package/bundle/boards/document-extract-demo-board.json +55 -0
- package/bundle/boards/dropitapp-board.json +138 -0
- package/bundle/boards/encode-demo-board.json +39 -0
- package/bundle/boards/encrypt-board.json +185 -0
- package/bundle/boards/encrypt-demo-board.json +39 -0
- package/bundle/boards/face-alert-board.json +181 -0
- package/bundle/boards/fetcher-demo-board.json +40 -0
- package/bundle/boards/fft-demo-board.json +28 -0
- package/bundle/boards/filter-demo-board.json +55 -0
- package/bundle/boards/flat-map-demo-board.json +42 -0
- package/bundle/boards/frame-limit-test-board.json +130 -0
- package/bundle/boards/game-board.json +413 -0
- package/bundle/boards/game-of-life-board.json +139 -0
- package/bundle/boards/game-of-life-demo-board.json +34 -0
- package/bundle/boards/gather-demo-board.json +62 -0
- package/bundle/boards/group-by-demo-board.json +62 -0
- package/bundle/boards/hacker-demo-board.json +45 -0
- package/bundle/boards/hacker-news-board.json +741 -0
- package/bundle/boards/hash-demo-board.json +40 -0
- package/bundle/boards/health-log-browser-demo-board.json +537 -0
- package/bundle/boards/health-log-demo-board.json +498 -0
- package/bundle/boards/helloworld-board.json +57 -0
- package/bundle/boards/hold-demo-board.json +67 -0
- package/bundle/boards/hold-scopes-demo-board.json +141 -0
- package/bundle/boards/http-client-browser-demo-board.json +379 -0
- package/bundle/boards/http-client-demo-board.json +380 -0
- package/bundle/boards/image-picker-demo-board.json +22 -0
- package/bundle/boards/imap-email-demo-board.json +35 -0
- package/bundle/boards/injector-demo-board.json +55 -0
- package/bundle/boards/iterator-demo-board.json +55 -0
- package/bundle/boards/limit-demo-board.json +66 -0
- package/bundle/boards/link-debugger.json +32 -0
- package/bundle/boards/live-location-demo-board.json +93 -0
- package/bundle/boards/live-radio-cloud-demo-board.json +216 -0
- package/bundle/boards/live-radio-cloud-direct-demo-board.json +311 -0
- package/bundle/boards/live-radio-demo-board.json +211 -0
- package/bundle/boards/live-radio-members-demo-board.json +314 -0
- package/bundle/boards/local-storage-demo-board.json +30 -0
- package/bundle/boards/lz-compress-demo-board.json +52 -0
- package/bundle/boards/map-demo-board.json +46 -0
- package/bundle/boards/match-filter-demo-board.json +43 -0
- package/bundle/boards/meeting-poll-browser-demo-board.json +649 -0
- package/bundle/boards/meeting-poll-demo-board.json +650 -0
- package/bundle/boards/microphone-monitor-demo-board.json +28 -0
- package/bundle/boards/microphone-speaker-demo-board.json +86 -0
- package/bundle/boards/monitor-demo-board.json +52 -0
- package/bundle/boards/mounted-endpoint-demo-board-hkp-rt.json +84 -0
- package/bundle/boards/mounted-endpoint-demo-board.json +84 -0
- package/bundle/boards/nested-rhythm-demo-board.json +1160 -0
- package/bundle/boards/next-bus-board.json +52 -0
- package/bundle/boards/next-bus-switch-board.json +79 -0
- package/bundle/boards/night-groove-demo-board.json +742 -0
- package/bundle/boards/noise-alert-board.json +146 -0
- package/bundle/boards/offer-followup-demo-board.json +291 -0
- package/bundle/boards/offer-intake-demo-board.json +289 -0
- package/bundle/boards/offer-intake-hetzner-demo-board.json +286 -0
- package/bundle/boards/offer-intake-imap-demo-board.json +289 -0
- package/bundle/boards/offer-intake-imap-hetzner-demo-board.json +354 -0
- package/bundle/boards/p2p-receiver-demo-board.json +65 -0
- package/bundle/boards/p2p-sender-demo-board.json +94 -0
- package/bundle/boards/pad-groove-demo-board.json +644 -0
- package/bundle/boards/peer-chat-board.json +109 -0
- package/bundle/boards/peer-chat-node-board.json +97 -0
- package/bundle/boards/qr-code-demo-board.json +22 -0
- package/bundle/boards/queue-demo-board.json +239 -0
- package/bundle/boards/reduce-demo-board.json +54 -0
- package/bundle/boards/remote-named-demo-board-hkp-rt.json +61 -0
- package/bundle/boards/remote-named-demo-board.json +61 -0
- package/bundle/boards/rss-demo-board.json +530 -0
- package/bundle/boards/rss-radio-board.json +515 -0
- package/bundle/boards/rss-radio-brief-one-board.json +237 -0
- package/bundle/boards/rss-radio-editor-unit-board.json +555 -0
- package/bundle/boards/rss-radio-library-unit-board.json +293 -0
- package/bundle/boards/rss-radio-unit-board.json +809 -0
- package/bundle/boards/rss-radio-voice-unit-board.json +146 -0
- package/bundle/boards/runtime-configs/hold-example.json +41 -0
- package/bundle/boards/runtime-configs/injector-example.json +23 -0
- package/bundle/boards/runtime-configs/speech-to-text-example.json +35 -0
- package/bundle/boards/runtime-configs/text-generation-example.json +31 -0
- package/bundle/boards/runtime-configs/text-to-speech-example.json +28 -0
- package/bundle/boards/runtime-configs/text-to-speech-inflect-example.json +28 -0
- package/bundle/boards/runtime-configs/ws-audio-playback-example.json +23 -0
- package/bundle/boards/select-demo-board.json +32 -0
- package/bundle/boards/send-ntfy-board.json +52 -0
- package/bundle/boards/shared-costs-demo-board.json +361 -0
- package/bundle/boards/sign-demo-board.json +41 -0
- package/bundle/boards/skill-router-demo-board.json +210 -0
- package/bundle/boards/smooth-demo-board.json +91 -0
- package/bundle/boards/smtp-email-demo-board.json +34 -0
- package/bundle/boards/sort-demo-board.json +55 -0
- package/bundle/boards/sound-demo-board.json +65 -0
- package/bundle/boards/spectral-modifier-board.json +98 -0
- package/bundle/boards/speech-synth-demo-board.json +37 -0
- package/bundle/boards/speech-to-text-demo-board.json +146 -0
- package/bundle/boards/spotify-board.json +75 -0
- package/bundle/boards/spread-demo-board.json +35 -0
- package/bundle/boards/sql-demo-board.json +56 -0
- package/bundle/boards/sql-explorer-board.json +447 -0
- package/bundle/boards/sql-explorer-node-board.json +474 -0
- package/bundle/boards/sql-import-board.json +156 -0
- package/bundle/boards/state-demo-board.json +65 -0
- package/bundle/boards/stopper-demo-board.json +40 -0
- package/bundle/boards/storage-demo-board.json +193 -0
- package/bundle/boards/store-demo-board.json +63 -0
- package/bundle/boards/swimlanes-demo-board.json +197 -0
- package/bundle/boards/switch-demo-board.json +51 -0
- package/bundle/boards/syn-board.json +19 -0
- package/bundle/boards/syn-booking-unit-board.json +998 -0
- package/bundle/boards/syn-conversations-anthropic-demo-board.json +925 -0
- package/bundle/boards/syn-conversations-demo-board.json +925 -0
- package/bundle/boards/syn-hotels-unit-board.json +287 -0
- package/bundle/boards/syn-mail-desk-board.json +25 -0
- package/bundle/boards/syn-mail-desk-unit-board.json +254 -0
- package/bundle/boards/telegram-listener-demo-board.json +31 -0
- package/bundle/boards/text-generation-anthropic-demo-board.json +66 -0
- package/bundle/boards/text-generation-demo-board.json +51 -0
- package/bundle/boards/text-generation-local-demo-board.json +80 -0
- package/bundle/boards/text-to-speech-demo-board.json +58 -0
- package/bundle/boards/timeline-demo-board.json +477 -0
- package/bundle/boards/timer-demo-board.json +35 -0
- package/bundle/boards/tracks-demo-board.json +95 -0
- package/bundle/boards/trigger-pad-demo-board.json +47 -0
- package/bundle/boards/triggerpad-board.json +31 -0
- package/bundle/boards/triggerpad-recorder-board.json +56 -0
- package/bundle/boards/uuid-generator-demo-board.json +166 -0
- package/bundle/boards/voice-assistant-demo-board.json +230 -0
- package/bundle/boards/voice-assistant-hkp-rt-demo-board.json +239 -0
- package/bundle/boards/voice-assistant-local-demo-board.json +232 -0
- package/bundle/boards/voice-assistant-skills-demo-board.json +316 -0
- package/bundle/boards/voice-assistant-skills-local-demo-board.json +316 -0
- package/bundle/boards/xy-pad-demo-board.json +36 -0
- package/bundle/docs/content/board-json.md +277 -0
- package/bundle/docs/content/boards/alpaca-board.md +56 -0
- package/bundle/docs/content/boards/animate-board.md +40 -0
- package/bundle/docs/content/boards/ascii-cam-board.md +37 -0
- package/bundle/docs/content/boards/audiolize-board.md +47 -0
- package/bundle/docs/content/boards/court-booking-browser-demo-board.md +43 -0
- package/bundle/docs/content/boards/court-booking-demo-board.md +120 -0
- package/bundle/docs/content/boards/current-time-board.md +32 -0
- package/bundle/docs/content/boards/dropitapp-board.md +57 -0
- package/bundle/docs/content/boards/encrypt-board.md +36 -0
- package/bundle/docs/content/boards/face-alert-board.md +50 -0
- package/bundle/docs/content/boards/game-board.md +69 -0
- package/bundle/docs/content/boards/game-of-life-board.md +47 -0
- package/bundle/docs/content/boards/hacker-news-board.md +76 -0
- package/bundle/docs/content/boards/health-log-browser-demo-board.md +56 -0
- package/bundle/docs/content/boards/health-log-demo-board.md +138 -0
- package/bundle/docs/content/boards/helloworld-board.md +40 -0
- package/bundle/docs/content/boards/link-debugger.md +31 -0
- package/bundle/docs/content/boards/live-radio-cloud-demo-board.md +70 -0
- package/bundle/docs/content/boards/live-radio-cloud-direct-demo-board.md +89 -0
- package/bundle/docs/content/boards/live-radio-demo-board.md +102 -0
- package/bundle/docs/content/boards/live-radio-members-demo-board.md +91 -0
- package/bundle/docs/content/boards/meeting-poll-browser-demo-board.md +45 -0
- package/bundle/docs/content/boards/meeting-poll-demo-board.md +121 -0
- package/bundle/docs/content/boards/mounted-endpoint-demo-board-hkp-rt.md +50 -0
- package/bundle/docs/content/boards/nested-rhythm-demo-board.md +165 -0
- package/bundle/docs/content/boards/next-bus-board.md +38 -0
- package/bundle/docs/content/boards/next-bus-switch-board.md +44 -0
- package/bundle/docs/content/boards/night-groove-demo-board.md +110 -0
- package/bundle/docs/content/boards/noise-alert-board.md +44 -0
- package/bundle/docs/content/boards/pad-groove-demo-board.md +93 -0
- package/bundle/docs/content/boards/peer-chat-board.md +49 -0
- package/bundle/docs/content/boards/peer-chat-node-board.md +47 -0
- package/bundle/docs/content/boards/rss-radio-board.md +221 -0
- package/bundle/docs/content/boards/send-ntfy-board.md +33 -0
- package/bundle/docs/content/boards/shared-costs-demo-board.md +65 -0
- package/bundle/docs/content/boards/spectral-modifier-board.md +55 -0
- package/bundle/docs/content/boards/spotify-board.md +46 -0
- package/bundle/docs/content/boards/sql-explorer-board.md +70 -0
- package/bundle/docs/content/boards/sql-explorer-node-board.md +76 -0
- package/bundle/docs/content/boards/sql-import-board.md +37 -0
- package/bundle/docs/content/boards/swimlanes-demo-board.md +148 -0
- package/bundle/docs/content/boards/syn-mail-desk-board.md +52 -0
- package/bundle/docs/content/boards/timeline-demo-board.md +115 -0
- package/bundle/docs/content/boards/triggerpad-board.md +32 -0
- package/bundle/docs/content/boards/triggerpad-recorder-board.md +52 -0
- package/bundle/docs/content/boards/voice-assistant-demo-board.md +51 -0
- package/bundle/docs/content/concepts/assets.md +245 -0
- package/bundle/docs/content/concepts/blocks.md +300 -0
- package/bundle/docs/content/concepts/board-assistant.md +307 -0
- package/bundle/docs/content/concepts/board.md +265 -0
- package/bundle/docs/content/concepts/cloud-boards.md +803 -0
- package/bundle/docs/content/concepts/coordinator.md +297 -0
- package/bundle/docs/content/concepts/logging.md +293 -0
- package/bundle/docs/content/concepts/mounts.md +314 -0
- package/bundle/docs/content/concepts/presets.md +360 -0
- package/bundle/docs/content/concepts/remotes.md +423 -0
- package/bundle/docs/content/concepts/runtime.md +664 -0
- package/bundle/docs/content/concepts/scopes.md +256 -0
- package/bundle/docs/content/concepts/service.md +227 -0
- package/bundle/docs/content/concepts/units.md +321 -0
- package/bundle/docs/content/introduction.md +163 -0
- package/bundle/docs/content/llm/circle-text-board.json +55 -0
- package/bundle/docs/content/llm/howto-workflow.md +369 -0
- package/bundle/docs/content/repository.md +161 -0
- package/bundle/docs/content/services/aggregator.md +76 -0
- package/bundle/docs/content/services/analyzer.md +44 -0
- package/bundle/docs/content/services/ascii-art.md +58 -0
- package/bundle/docs/content/services/asset.md +88 -0
- package/bundle/docs/content/services/audio-editor.md +57 -0
- package/bundle/docs/content/services/audio-encode.md +115 -0
- package/bundle/docs/content/services/audio-io.md +173 -0
- package/bundle/docs/content/services/batcher.md +49 -0
- package/bundle/docs/content/services/board-service.md +48 -0
- package/bundle/docs/content/services/browser-sub-service.md +75 -0
- package/bundle/docs/content/services/buffer.md +70 -0
- package/bundle/docs/content/services/cache.md +92 -0
- package/bundle/docs/content/services/camera.md +85 -0
- package/bundle/docs/content/services/canvas.md +196 -0
- package/bundle/docs/content/services/changes.md +87 -0
- package/bundle/docs/content/services/chunked-file-provider.md +63 -0
- package/bundle/docs/content/services/communication-dispatcher.md +190 -0
- package/bundle/docs/content/services/configurator.md +66 -0
- package/bundle/docs/content/services/conversations.md +244 -0
- package/bundle/docs/content/services/debounce.md +45 -0
- package/bundle/docs/content/services/decrypt.md +53 -0
- package/bundle/docs/content/services/delay.md +55 -0
- package/bundle/docs/content/services/detect.md +101 -0
- package/bundle/docs/content/services/document-extract.md +174 -0
- package/bundle/docs/content/services/download.md +67 -0
- package/bundle/docs/content/services/encode.md +63 -0
- package/bundle/docs/content/services/encrypt.md +53 -0
- package/bundle/docs/content/services/fetcher.md +122 -0
- package/bundle/docs/content/services/fft.md +96 -0
- package/bundle/docs/content/services/file-source.md +67 -0
- package/bundle/docs/content/services/filesystem.md +157 -0
- package/bundle/docs/content/services/filter.md +133 -0
- package/bundle/docs/content/services/flat-map.md +62 -0
- package/bundle/docs/content/services/game-of-life.md +93 -0
- package/bundle/docs/content/services/gather.md +135 -0
- package/bundle/docs/content/services/gif-encoder.md +56 -0
- package/bundle/docs/content/services/github.md +85 -0
- package/bundle/docs/content/services/group-by.md +54 -0
- package/bundle/docs/content/services/hacker.md +79 -0
- package/bundle/docs/content/services/hash.md +59 -0
- package/bundle/docs/content/services/hold.md +182 -0
- package/bundle/docs/content/services/http-relay-client.md +48 -0
- package/bundle/docs/content/services/http-uploader.md +62 -0
- package/bundle/docs/content/services/http.md +424 -0
- package/bundle/docs/content/services/if.md +79 -0
- package/bundle/docs/content/services/ifft.md +46 -0
- package/bundle/docs/content/services/image-picker.md +50 -0
- package/bundle/docs/content/services/imap-email.md +104 -0
- package/bundle/docs/content/services/injector.md +129 -0
- package/bundle/docs/content/services/input.md +90 -0
- package/bundle/docs/content/services/iterator.md +186 -0
- package/bundle/docs/content/services/join.md +153 -0
- package/bundle/docs/content/services/limit.md +47 -0
- package/bundle/docs/content/services/local-storage.md +47 -0
- package/bundle/docs/content/services/looper.md +62 -0
- package/bundle/docs/content/services/lz-compress.md +52 -0
- package/bundle/docs/content/services/map.md +257 -0
- package/bundle/docs/content/services/match-filter.md +38 -0
- package/bundle/docs/content/services/microphone-monitor.md +66 -0
- package/bundle/docs/content/services/monitor.md +67 -0
- package/bundle/docs/content/services/ollama.md +116 -0
- package/bundle/docs/content/services/openai-prompt.md +74 -0
- package/bundle/docs/content/services/output.md +107 -0
- package/bundle/docs/content/services/peer-socket.md +61 -0
- package/bundle/docs/content/services/qr-code.md +48 -0
- package/bundle/docs/content/services/queue.md +201 -0
- package/bundle/docs/content/services/reduce.md +51 -0
- package/bundle/docs/content/services/rss.md +232 -0
- package/bundle/docs/content/services/select.md +47 -0
- package/bundle/docs/content/services/sign.md +53 -0
- package/bundle/docs/content/services/skill-router.md +154 -0
- package/bundle/docs/content/services/smooth.md +59 -0
- package/bundle/docs/content/services/smtp-email.md +129 -0
- package/bundle/docs/content/services/sort.md +48 -0
- package/bundle/docs/content/services/sound.md +87 -0
- package/bundle/docs/content/services/speech-synth.md +46 -0
- package/bundle/docs/content/services/speech-to-text.md +172 -0
- package/bundle/docs/content/services/spotify.md +77 -0
- package/bundle/docs/content/services/spread.md +111 -0
- package/bundle/docs/content/services/sql.md +491 -0
- package/bundle/docs/content/services/state.md +57 -0
- package/bundle/docs/content/services/static.md +47 -0
- package/bundle/docs/content/services/stopper.md +55 -0
- package/bundle/docs/content/services/storage.md +155 -0
- package/bundle/docs/content/services/store.md +255 -0
- package/bundle/docs/content/services/sub-service.md +152 -0
- package/bundle/docs/content/services/switch.md +67 -0
- package/bundle/docs/content/services/telegram-listener.md +76 -0
- package/bundle/docs/content/services/text-generation.md +390 -0
- package/bundle/docs/content/services/text-to-speech.md +130 -0
- package/bundle/docs/content/services/timeline.md +333 -0
- package/bundle/docs/content/services/timer.md +113 -0
- package/bundle/docs/content/services/tracks.md +207 -0
- package/bundle/docs/content/services/trigger-pad.md +68 -0
- package/bundle/docs/content/services/uuid-generator.md +48 -0
- package/bundle/docs/content/services/wav-reader.md +66 -0
- package/bundle/docs/content/services/websocket.md +189 -0
- package/bundle/docs/content/services/workflow-board-builder.md +65 -0
- package/bundle/docs/content/services/xy-pad.md +60 -0
- package/bundle/docs/content/targets.md +434 -0
- package/bundle/docs/content/testing.md +263 -0
- package/bundle/docs/content/vocabulary.md +171 -0
- package/bundle/hkp-frontend/src/runtime/browser/BrowserRegistry.tsx +141 -0
- package/bundle/hkp-frontend/src/runtime/browser/BrowserRuntime.tsx +202 -0
- package/bundle/hkp-frontend/src/runtime/browser/BrowserRuntimeApi.ts +306 -0
- package/bundle/hkp-frontend/src/runtime/browser/BrowserRuntimeApp.ts +310 -0
- package/bundle/hkp-frontend/src/runtime/browser/BrowserRuntimeScope.ts +500 -0
- package/bundle/hkp-frontend/src/runtime/browser/UIRegistry.ts +150 -0
- package/bundle/hkp-frontend/src/runtime/browser/assetResolver.ts +123 -0
- package/bundle/hkp-frontend/src/runtime/browser/pipelineNames.ts +65 -0
- package/bundle/hkp-frontend/src/runtime/browser/registry/Default.ts +237 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Aggregator.ts +127 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AggregatorUI.tsx +83 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Analyzer.tsx +43 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AnalyzerUI/Bars.tsx +33 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AnalyzerUI/HorizontalSpectrogram.tsx +53 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AnalyzerUI/VerticalSpectogram.tsx +56 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AnalyzerUI/Waveform.tsx +109 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AnalyzerUI/index.tsx +99 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AsciiArt.ts +119 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AsciiArtUI.tsx +131 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Asset.ts +125 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AssetUI.tsx +37 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioDeviceSettings.tsx +56 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioEditor.ts +129 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioEditorUI.tsx +549 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioInput.ts +298 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioInputUI.tsx +257 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioOutput.ts +189 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioOutputUI.tsx +63 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Batcher.ts +11 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BoardService.tsx +86 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BoardServiceUI.tsx +410 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BrowserGather.ts +311 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BrowserIterator.ts +167 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BrowserSubService.tsx +583 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BrowserSubServiceUI.tsx +187 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BrowserTracks.tsx +477 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Buffer.ts +11 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Cache.ts +134 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CacheUI.tsx +125 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Camera.ts +90 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CameraUI.tsx +321 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Canvas.ts +139 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/canvasDraw.ts +543 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/canvasUtils.ts +39 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/index.tsx +287 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/tests/canvasDraw.test.ts +355 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/tests/canvasUtils.test.ts +89 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/tests/fullscreen.test.tsx +26 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Changes.ts +201 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/ChangesUI.tsx +85 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/ChunkedFileProvider.tsx +183 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Configurator.ts +258 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/ConfiguratorUI.tsx +83 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/ConsideredHacker.tsx +28 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Debounce.ts +75 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/DebounceUI.tsx +70 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Decrypt.ts +127 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/DecryptUI.tsx +39 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Delay.tsx +110 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Detect.ts +214 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/DetectUI.tsx +86 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Download.ts +120 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/DownloadUI.tsx +61 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Encode.ts +138 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/EncodeUI.tsx +52 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Encrypt.ts +125 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/EncryptUI.tsx +39 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FFT.tsx +238 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FFTUI.tsx +112 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FeedbackService.ts +247 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FeedbackServiceUI.tsx +35 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Fetcher.tsx +232 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FetcherUI.tsx +154 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FileSource.tsx +171 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Filter.tsx +18 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FilterUI.tsx +89 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FlatMap.ts +68 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FlatMapUI.tsx +38 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GameOfLife.ts +210 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GameOfLifeRenderer.ts +182 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GameOfLifeRendererUI.tsx +48 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GameOfLifeUI.tsx +58 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GatherUI.tsx +84 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GifEncoder.tsx +651 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GithubAPI.ts +294 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GithubComponents.tsx +389 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GithubSink.tsx +109 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GithubSinkUI.tsx +143 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GithubSource.tsx +86 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GithubSourceUI.tsx +162 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GroupBy.ts +11 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HTTPUploader.ts +102 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Hacker.ts +73 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HackerUI.tsx +134 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Hash.ts +134 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HashUI.tsx +45 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Hold.ts +288 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HoldUI.tsx +43 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HttpClient.ts +558 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HttpClientUI.tsx +212 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HttpUploaderTokenInjector.ts +149 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/IfService.ts +231 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/IfServiceUI.tsx +50 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/ImagePicker.tsx +118 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Injector.ts +61 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/InjectorUI.tsx +389 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Input.ts +214 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/InputUI.tsx +71 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/IteratorUI.tsx +59 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/LZCompress.ts +63 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Limit.ts +39 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/LimitUI.tsx +41 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/LocalStorage.ts +65 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/LocalStorageUI.tsx +72 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Map.ts +14 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/MapUI.tsx +334 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/MapUIModeAssist.tsx +61 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/MatchFilter.ts +11 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/MicrophoneMonitor.ts +190 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/MicrophoneMonitorUI.tsx +94 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Monitor.tsx +15 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/MonitorUI.tsx +281 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OllamaPrompt.ts +237 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OllamaPromptUI/BusyIndicator.tsx +18 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OllamaPromptUI/PromptInput.tsx +71 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OllamaPromptUI/StreamingPreview.tsx +24 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OllamaPromptUI/index.tsx +156 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OpenAIPrompt.ts +254 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OpenAIPromptUI.tsx +189 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Output.tsx +145 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OutputUI.tsx +118 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/PeerChatTemplate.json +43 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/PeerConnection.ts +260 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/PeerSocket.tsx +268 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/PeerSocketReceiverTemplate.json +29 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/PeerSocketUI.tsx +341 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/PlaceholderUI.tsx +72 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/QrCode.ts +63 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/QrCodeUI.tsx +197 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Select.ts +40 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SelectUI.tsx +41 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/ServiceBase.ts +50 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SetRuntimeVariable.ts +65 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SetRuntimeVariableUI.tsx +50 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Sign.ts +182 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SignUI.tsx +55 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Smooth.ts +101 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SmoothUI.tsx +50 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Sort.ts +59 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SortUI.tsx +61 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Sound.ts +541 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SoundUI.tsx +183 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SpeechSynth.ts +56 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SpeechSynthUI.tsx +108 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Spotify.tsx +111 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SpotifyAPI.ts +113 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SpotifyComponents.tsx +104 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SpotifyUI.tsx +186 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Spread.ts +117 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SpreadUI.tsx +38 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Sql.ts +448 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SqlInspector.tsx +332 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SqlUI.tsx +226 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/State.ts +149 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Stopper.ts +29 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Switch.tsx +419 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SwitchUI.tsx +171 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TickRuleDelta.ts +101 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TickRuleDeltaUI.tsx +56 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Timeline.ts +709 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/TimelinePanel.tsx +79 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/TimelineRows.tsx +574 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/Transport.tsx +141 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/index.tsx +119 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/model.ts +425 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/parts.tsx +708 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Timer.ts +15 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimerUI.tsx +213 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TracksUI.tsx +81 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TriggerPad.ts +108 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TriggerPadUI.tsx +160 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/UuidGenerator.ts +74 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/UuidGeneratorUI.tsx +59 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/WaveformDisplay.tsx +104 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/WebsocketClient.ts +114 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/WebsocketClientUI.tsx +42 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/WorkflowBoardBuilder.ts +499 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/WorkflowBoardBuilderUI.tsx +521 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/XYPad.ts +68 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/XYPadUI.tsx +274 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/audio/audioHelper.ts +36 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Batcher.ts +76 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Buffer.ts +142 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Filter.ts +125 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/GroupBy.ts +101 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Map.ts +543 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/MatchFilter.ts +49 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Monitor.ts +75 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Reduce.ts +74 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Timer.ts +342 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/bundle.js +7375 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/eval.ts +208 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/expressionEval.ts +223 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/helpers.ts +10 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/index.ts +37 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/camera/videoCapture.ts +78 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/changes-modes.ts +9 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/colorpalette.ts +26 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/detect/DetectionPreview.tsx +76 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/detect/mediapipe.ts +34 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/detect/models/README.md +14 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/detect/models/blaze_face_short_range.tflite +0 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/detect-modes.ts +7 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/encode-modes.ts +10 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/helpers.ts +300 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/http-client-methods.ts +10 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/sql-database.ts +594 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/sql-dump.ts +192 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/sql-inspect.ts +130 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/sql-inspector.css +155 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/sql-modes.ts +27 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/TESTING_APPROACH.md +169 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/__snapshots__/service-ui.future-enhancements.test.tsx.snap +5 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/audio-input-permissions.test.ts +42 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/audiolizeLanes.test.ts +70 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/browser-scopes.test.ts +305 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/cache-service.test.ts +53 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/camera-video-capture.test.ts +24 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/changes-service.test.ts +133 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/crypto-secrets.test.ts +196 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/detect-service.test.ts +119 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/download.test.ts +100 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/expressionEval.test.ts +117 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/fetcher-panel.test.tsx +35 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/fetcher-service.test.ts +80 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/filter-service.test.ts +37 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/github-secrets.test.ts +108 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/group-by-board.test.ts +42 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/hold-service.test.ts +199 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/http-client-service.test.ts +474 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/http-uploader-token-injector.test.ts +152 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/iterator-gather-service.test.ts +277 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/map-service.test.ts +1047 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/map-ui.test.tsx +70 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/nested-rhythm-board.test.ts +311 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/night-groove-board.test.ts +290 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/pad-groove-board.test.ts +230 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/peer-host-discovery.test.ts +77 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/peer-socket-discovery.test.ts +69 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/pipeline-names.test.ts +63 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/secret-canary.test.ts +121 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/service-modules.contract.test.ts +229 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/service-ui.advanced.test.tsx +670 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/service-ui.future-enhancements.test.tsx +219 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/service-ui.integration.test.tsx +432 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/service-ui.smoke.test.tsx +140 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/sound-service.test.ts +293 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/spread-service.test.ts +137 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/sql-dump.test.ts +172 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/sql-inspect.test.ts +163 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/sql-inspector.test.tsx +121 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/sql.test.ts +702 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/sub-service-in-block.test.tsx +84 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/switch-service.test.ts +254 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/timeline-board.test.ts +218 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/timeline-service.test.ts +1002 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/timeline-ui-model.test.ts +230 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/timeline-ui.test.tsx +371 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/timer-service.test.ts +902 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/tracks-service.test.ts +275 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/tracks-ui.test.tsx +126 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/triggerpad-recorder-board.test.ts +100 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/uuid-generator-service.test.ts +214 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/timeline-core.ts +410 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/workflow-prompt/DefaultWorkflowBoard.ts +90 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/workflow-prompt/RefinementSession.ts +121 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/workflow-prompt/SystemPromptCatalog.ts +245 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/workflow-prompt/catalog/browser-service-specs.json +110 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/workflow-prompt/catalog/runtime-capabilities.json +37 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/asset-service.test.ts +73 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/board-persistence.integration.test.tsx +391 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/browser-runtime.identity-binding.test.tsx +81 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/demo-boards.regression.test.tsx +386 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/failed-call-lifecycle.test.ts +56 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/http-client.integration.test.ts +146 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/nested-run.integration.test.ts +415 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/replay-next.test.ts +391 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/rerun.test.ts +71 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/run-no-input.test.ts +72 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/runtime-add.integration.test.tsx +256 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/runtime-operations.integration.test.tsx +310 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/runtime-processing.integration.test.ts +311 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/runtime-removal.integration.test.tsx +454 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/runtime-update.identity.integration.test.tsx +145 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/service-operations.integration.test.tsx +511 -0
- package/bundle/hkp-node/src/address.ts +72 -0
- package/bundle/hkp-node/src/assets.ts +582 -0
- package/bundle/hkp-node/src/auth.ts +369 -0
- package/bundle/hkp-node/src/callReport.ts +281 -0
- package/bundle/hkp-node/src/coordinator/auth.ts +85 -0
- package/bundle/hkp-node/src/coordinator/binaryFrame.ts +189 -0
- package/bundle/hkp-node/src/coordinator/boardStore.ts +0 -0
- package/bundle/hkp-node/src/coordinator/bridge.ts +242 -0
- package/bundle/hkp-node/src/coordinator/bridgeProtocol.ts +200 -0
- package/bundle/hkp-node/src/coordinator/coordinator.ts +594 -0
- package/bundle/hkp-node/src/coordinator/facadeAccess.ts +426 -0
- package/bundle/hkp-node/src/coordinator/fileBoardStore.ts +159 -0
- package/bundle/hkp-node/src/coordinator/frameLimits.ts +41 -0
- package/bundle/hkp-node/src/coordinator/index.ts +12 -0
- package/bundle/hkp-node/src/coordinator/join.ts +62 -0
- package/bundle/hkp-node/src/coordinator/logStore.ts +335 -0
- package/bundle/hkp-node/src/coordinator/members.ts +133 -0
- package/bundle/hkp-node/src/coordinator/mount.ts +124 -0
- package/bundle/hkp-node/src/coordinator/participantProtocol.ts +169 -0
- package/bundle/hkp-node/src/coordinator/participants.ts +666 -0
- package/bundle/hkp-node/src/coordinator/router.ts +416 -0
- package/bundle/hkp-node/src/coordinator/session.ts +1610 -0
- package/bundle/hkp-node/src/coordinator/types.ts +95 -0
- package/bundle/hkp-node/src/coordinatorLinks.ts +712 -0
- package/bundle/hkp-node/src/credentialedFetch.ts +122 -0
- package/bundle/hkp-node/src/egress.ts +344 -0
- package/bundle/hkp-node/src/expressionEval.ts +223 -0
- package/bundle/hkp-node/src/index.ts +470 -0
- package/bundle/hkp-node/src/instance.ts +172 -0
- package/bundle/hkp-node/src/mounts.ts +296 -0
- package/bundle/hkp-node/src/origins.ts +201 -0
- package/bundle/hkp-node/src/reset.ts +187 -0
- package/bundle/hkp-node/src/runtime.ts +1345 -0
- package/bundle/hkp-node/src/secrets.ts +292 -0
- package/bundle/hkp-node/src/server.ts +2120 -0
- package/bundle/hkp-node/src/services/asset.ts +150 -0
- package/bundle/hkp-node/src/services/claudeModels.ts +370 -0
- package/bundle/hkp-node/src/services/communication-dispatcher.ts +735 -0
- package/bundle/hkp-node/src/services/conversations.ts +914 -0
- package/bundle/hkp-node/src/services/database.ts +386 -0
- package/bundle/hkp-node/src/services/document-extract.ts +575 -0
- package/bundle/hkp-node/src/services/expression.ts +417 -0
- package/bundle/hkp-node/src/services/fileStore.ts +369 -0
- package/bundle/hkp-node/src/services/filesystem.ts +280 -0
- package/bundle/hkp-node/src/services/gather.ts +335 -0
- package/bundle/hkp-node/src/services/hold.ts +275 -0
- package/bundle/hkp-node/src/services/http-client.ts +525 -0
- package/bundle/hkp-node/src/services/http-server.ts +1222 -0
- package/bundle/hkp-node/src/services/imap-email.ts +627 -0
- package/bundle/hkp-node/src/services/injector.ts +135 -0
- package/bundle/hkp-node/src/services/iterator.ts +203 -0
- package/bundle/hkp-node/src/services/join.ts +226 -0
- package/bundle/hkp-node/src/services/map.ts +632 -0
- package/bundle/hkp-node/src/services/monitor.ts +128 -0
- package/bundle/hkp-node/src/services/nested-pipeline.ts +405 -0
- package/bundle/hkp-node/src/services/peer-server.ts +268 -0
- package/bundle/hkp-node/src/services/queue-store.ts +301 -0
- package/bundle/hkp-node/src/services/queue.ts +316 -0
- package/bundle/hkp-node/src/services/recordStore.ts +341 -0
- package/bundle/hkp-node/src/services/rss-parse.ts +281 -0
- package/bundle/hkp-node/src/services/rss.ts +392 -0
- package/bundle/hkp-node/src/services/smtp-email.ts +398 -0
- package/bundle/hkp-node/src/services/spread.ts +153 -0
- package/bundle/hkp-node/src/services/sql-dump.ts +192 -0
- package/bundle/hkp-node/src/services/sql.ts +429 -0
- package/bundle/hkp-node/src/services/stopper.ts +69 -0
- package/bundle/hkp-node/src/services/storage.ts +332 -0
- package/bundle/hkp-node/src/services/store.ts +609 -0
- package/bundle/hkp-node/src/services/stream.ts +367 -0
- package/bundle/hkp-node/src/services/sub-service.ts +600 -0
- package/bundle/hkp-node/src/services/telegram-listener.ts +263 -0
- package/bundle/hkp-node/src/services/telegram-sender.ts +136 -0
- package/bundle/hkp-node/src/services/text-generation.ts +1249 -0
- package/bundle/hkp-node/src/services/timer.ts +273 -0
- package/bundle/hkp-node/src/services/tracks.ts +381 -0
- package/bundle/hkp-node/src/services/websocket-reader.ts +364 -0
- package/bundle/hkp-node/src/types.ts +465 -0
- package/bundle/hkp-node/src/wsTickets.ts +77 -0
- package/bundle/hkp-node/src/yas.ts +95 -0
- package/bundle/hkp-python/src/hkp/__init__.py +1 -0
- package/bundle/hkp-python/src/hkp/__main__.py +241 -0
- package/bundle/hkp-python/src/hkp/address.py +56 -0
- package/bundle/hkp-python/src/hkp/assets.py +439 -0
- package/bundle/hkp-python/src/hkp/auth.py +252 -0
- package/bundle/hkp-python/src/hkp/binary_frame.py +100 -0
- package/bundle/hkp-python/src/hkp/call_report.py +201 -0
- package/bundle/hkp-python/src/hkp/coordinator_links.py +667 -0
- package/bundle/hkp-python/src/hkp/data.py +118 -0
- package/bundle/hkp-python/src/hkp/egress.py +310 -0
- package/bundle/hkp-python/src/hkp/mount.py +46 -0
- package/bundle/hkp-python/src/hkp/mounts.py +288 -0
- package/bundle/hkp-python/src/hkp/origins.py +169 -0
- package/bundle/hkp-python/src/hkp/runtime.py +1131 -0
- package/bundle/hkp-python/src/hkp/secrets.py +261 -0
- package/bundle/hkp-python/src/hkp/server.py +1714 -0
- package/bundle/hkp-python/src/hkp/services/__init__.py +0 -0
- package/bundle/hkp-python/src/hkp/services/asset.py +123 -0
- package/bundle/hkp-python/src/hkp/services/audio_encode.py +190 -0
- package/bundle/hkp-python/src/hkp/services/expression.py +942 -0
- package/bundle/hkp-python/src/hkp/services/gather.py +266 -0
- package/bundle/hkp-python/src/hkp/services/hold.py +246 -0
- package/bundle/hkp-python/src/hkp/services/http_client.py +444 -0
- package/bundle/hkp-python/src/hkp/services/http_server.py +790 -0
- package/bundle/hkp-python/src/hkp/services/iterator.py +141 -0
- package/bundle/hkp-python/src/hkp/services/join.py +173 -0
- package/bundle/hkp-python/src/hkp/services/map_service.py +452 -0
- package/bundle/hkp-python/src/hkp/services/monitor.py +99 -0
- package/bundle/hkp-python/src/hkp/services/nested_pipeline.py +278 -0
- package/bundle/hkp-python/src/hkp/services/skill_router.py +388 -0
- package/bundle/hkp-python/src/hkp/services/speech_to_text.py +186 -0
- package/bundle/hkp-python/src/hkp/services/spread.py +114 -0
- package/bundle/hkp-python/src/hkp/services/stopper.py +59 -0
- package/bundle/hkp-python/src/hkp/services/sub_service.py +417 -0
- package/bundle/hkp-python/src/hkp/services/text_generation.py +498 -0
- package/bundle/hkp-python/src/hkp/services/text_to_speech.py +237 -0
- package/bundle/hkp-python/src/hkp/services/timer.py +267 -0
- package/bundle/hkp-python/src/hkp/services/tracks.py +251 -0
- package/bundle/hkp-python/src/hkp/types.py +412 -0
- package/bundle/hkp-python/src/hkp/ws_tickets.py +54 -0
- package/bundle/hkp-python/src/hkp/yas.py +192 -0
- package/bundle/hkp-rt/lib/CMakeLists.txt +193 -0
- package/bundle/hkp-rt/lib/hkp-bundles/CMakeLists.txt +55 -0
- package/bundle/hkp-rt/lib/hkp-bundles/plugin_register.cpp +14 -0
- package/bundle/hkp-rt/lib/include/app.h +201 -0
- package/bundle/hkp-rt/lib/include/assets.h +239 -0
- package/bundle/hkp-rt/lib/include/auth.h +167 -0
- package/bundle/hkp-rt/lib/include/call_report.h +197 -0
- package/bundle/hkp-rt/lib/include/coordinator_links.h +127 -0
- package/bundle/hkp-rt/lib/include/discovery_hooks.h +29 -0
- package/bundle/hkp-rt/lib/include/egress.h +379 -0
- package/bundle/hkp-rt/lib/include/files.h +275 -0
- package/bundle/hkp-rt/lib/include/log_entry.h +131 -0
- package/bundle/hkp-rt/lib/include/mounts.h +139 -0
- package/bundle/hkp-rt/lib/include/origins.h +363 -0
- package/bundle/hkp-rt/lib/include/registry.h +119 -0
- package/bundle/hkp-rt/lib/include/secrets.h +414 -0
- package/bundle/hkp-rt/lib/include/server.h +122 -0
- package/bundle/hkp-rt/lib/include/service.h +190 -0
- package/bundle/hkp-rt/lib/include/slot_store.h +41 -0
- package/bundle/hkp-rt/lib/include/standalone_config.h +253 -0
- package/bundle/hkp-rt/lib/include/types/audio.h +3 -0
- package/bundle/hkp-rt/lib/include/types/data.h +152 -0
- package/bundle/hkp-rt/lib/include/types/message.h +56 -0
- package/bundle/hkp-rt/lib/include/types/models.h +154 -0
- package/bundle/hkp-rt/lib/include/types/owns_me.h +19 -0
- package/bundle/hkp-rt/lib/include/types/ringbuffer.h +86 -0
- package/bundle/hkp-rt/lib/include/types/types.h +33 -0
- package/bundle/hkp-rt/lib/include/types/validation.h +205 -0
- package/bundle/hkp-rt/lib/src/address.h +88 -0
- package/bundle/hkp-rt/lib/src/app.cpp +591 -0
- package/bundle/hkp-rt/lib/src/assets.cpp +686 -0
- package/bundle/hkp-rt/lib/src/binary_frame.h +154 -0
- package/bundle/hkp-rt/lib/src/common/expression.h +1481 -0
- package/bundle/hkp-rt/lib/src/common/inja.h +36 -0
- package/bundle/hkp-rt/lib/src/common/link_socket.cpp +437 -0
- package/bundle/hkp-rt/lib/src/common/link_socket.h +97 -0
- package/bundle/hkp-rt/lib/src/common/realtime_wakeup.h +120 -0
- package/bundle/hkp-rt/lib/src/common/stream_broadcast.h +250 -0
- package/bundle/hkp-rt/lib/src/common/string_util.h +31 -0
- package/bundle/hkp-rt/lib/src/common/websocket_client_session.cpp +217 -0
- package/bundle/hkp-rt/lib/src/common/websocket_client_session.h +37 -0
- package/bundle/hkp-rt/lib/src/common/websocket_config.h +119 -0
- package/bundle/hkp-rt/lib/src/common/websocket_protocol.h +32 -0
- package/bundle/hkp-rt/lib/src/common/websocket_server.cpp +82 -0
- package/bundle/hkp-rt/lib/src/common/websocket_server.h +57 -0
- package/bundle/hkp-rt/lib/src/common/websocket_server_listener.cpp +115 -0
- package/bundle/hkp-rt/lib/src/common/websocket_server_listener.h +35 -0
- package/bundle/hkp-rt/lib/src/common/websocket_server_session.cpp +172 -0
- package/bundle/hkp-rt/lib/src/common/websocket_server_session.h +53 -0
- package/bundle/hkp-rt/lib/src/common.h +13 -0
- package/bundle/hkp-rt/lib/src/coordinator_links.cpp +1142 -0
- package/bundle/hkp-rt/lib/src/discovery/discovery.cpp +707 -0
- package/bundle/hkp-rt/lib/src/discovery/discovery.h +89 -0
- package/bundle/hkp-rt/lib/src/http/auth.cpp +569 -0
- package/bundle/hkp-rt/lib/src/http/front_door.cpp +518 -0
- package/bundle/hkp-rt/lib/src/http/front_door.h +131 -0
- package/bundle/hkp-rt/lib/src/http/server.cpp +1539 -0
- package/bundle/hkp-rt/lib/src/http/ws_tickets.h +125 -0
- package/bundle/hkp-rt/lib/src/mount.h +63 -0
- package/bundle/hkp-rt/lib/src/process_context.h +335 -0
- package/bundle/hkp-rt/lib/src/process_depth.h +20 -0
- package/bundle/hkp-rt/lib/src/registry.cpp +233 -0
- package/bundle/hkp-rt/lib/src/runtime.cpp +986 -0
- package/bundle/hkp-rt/lib/src/runtime.h +225 -0
- package/bundle/hkp-rt/lib/src/runtime_host.h +180 -0
- package/bundle/hkp-rt/lib/src/service.cpp +315 -0
- package/bundle/hkp-rt/lib/src/services/asset.h +185 -0
- package/bundle/hkp-rt/lib/src/services/audio_encode.cpp +372 -0
- package/bundle/hkp-rt/lib/src/services/audio_encode.h +97 -0
- package/bundle/hkp-rt/lib/src/services/cache.h +362 -0
- package/bundle/hkp-rt/lib/src/services/cache_subservices.h +463 -0
- package/bundle/hkp-rt/lib/src/services/core_audio.cpp +384 -0
- package/bundle/hkp-rt/lib/src/services/core_audio.h +22 -0
- package/bundle/hkp-rt/lib/src/services/core_input.h +337 -0
- package/bundle/hkp-rt/lib/src/services/core_output.h +370 -0
- package/bundle/hkp-rt/lib/src/services/ffmpeg/detail.cpp +749 -0
- package/bundle/hkp-rt/lib/src/services/ffmpeg/detail.h +29 -0
- package/bundle/hkp-rt/lib/src/services/ffmpeg/ffmpeg.cpp +193 -0
- package/bundle/hkp-rt/lib/src/services/ffmpeg/ffmpeg.h +40 -0
- package/bundle/hkp-rt/lib/src/services/ffmpeg/types.h +120 -0
- package/bundle/hkp-rt/lib/src/services/fft_service.h +190 -0
- package/bundle/hkp-rt/lib/src/services/filesystem.h +309 -0
- package/bundle/hkp-rt/lib/src/services/filter.h +208 -0
- package/bundle/hkp-rt/lib/src/services/gather.h +272 -0
- package/bundle/hkp-rt/lib/src/services/hold.h +304 -0
- package/bundle/hkp-rt/lib/src/services/http_client.cpp +347 -0
- package/bundle/hkp-rt/lib/src/services/http_client.h +67 -0
- package/bundle/hkp-rt/lib/src/services/http_client_impl.h +45 -0
- package/bundle/hkp-rt/lib/src/services/http_client_template_examples.md +202 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_listener.cpp +130 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_listener.h +33 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_server.cpp +351 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_server.h +79 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_server_impl.cpp +230 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_server_impl.h +64 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_server_subservices.cpp +950 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_server_subservices.h +253 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_session.cpp +495 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_session.h +169 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_stream_listener.cpp +270 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_stream_listener.h +104 -0
- package/bundle/hkp-rt/lib/src/services/http_server/request_decode.h +164 -0
- package/bundle/hkp-rt/lib/src/services/if_service.h +210 -0
- package/bundle/hkp-rt/lib/src/services/ifft_service.h +198 -0
- package/bundle/hkp-rt/lib/src/services/injector.h +162 -0
- package/bundle/hkp-rt/lib/src/services/iterator.h +201 -0
- package/bundle/hkp-rt/lib/src/services/map.h +641 -0
- package/bundle/hkp-rt/lib/src/services/monitor.h +161 -0
- package/bundle/hkp-rt/lib/src/services/mp3_frames.h +119 -0
- package/bundle/hkp-rt/lib/src/services/mp4_to_wav.cpp +281 -0
- package/bundle/hkp-rt/lib/src/services/mp4_to_wav.h +263 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_registry.cpp +88 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_registry.h +43 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_server.h +163 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_server_listener.cpp +133 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_server_listener.h +46 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_server_session.cpp +165 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_server_session.h +44 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_ws_session.cpp +184 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_ws_session.h +55 -0
- package/bundle/hkp-rt/lib/src/services/receiver_session.h +172 -0
- package/bundle/hkp-rt/lib/src/services/root_certificates.h +3959 -0
- package/bundle/hkp-rt/lib/src/services/sha256_openssl.h +49 -0
- package/bundle/hkp-rt/lib/src/services/speech_http.h +306 -0
- package/bundle/hkp-rt/lib/src/services/speech_to_text.cpp +595 -0
- package/bundle/hkp-rt/lib/src/services/speech_to_text.h +69 -0
- package/bundle/hkp-rt/lib/src/services/static.cpp +45 -0
- package/bundle/hkp-rt/lib/src/services/static.h +41 -0
- package/bundle/hkp-rt/lib/src/services/stopper.h +72 -0
- package/bundle/hkp-rt/lib/src/services/sub_service.h +286 -0
- package/bundle/hkp-rt/lib/src/services/text_generation.cpp +1132 -0
- package/bundle/hkp-rt/lib/src/services/text_generation.h +71 -0
- package/bundle/hkp-rt/lib/src/services/text_to_speech.cpp +853 -0
- package/bundle/hkp-rt/lib/src/services/text_to_speech.h +97 -0
- package/bundle/hkp-rt/lib/src/services/timer.h +300 -0
- package/bundle/hkp-rt/lib/src/services/tracks.h +397 -0
- package/bundle/hkp-rt/lib/src/services/tts_inflect.cpp +149 -0
- package/bundle/hkp-rt/lib/src/services/tts_inflect.h +82 -0
- package/bundle/hkp-rt/lib/src/services/wav_reader.h +213 -0
- package/bundle/hkp-rt/lib/src/services/websocket_client_service.h +182 -0
- package/bundle/hkp-rt/lib/src/services/websocket_reader.h +148 -0
- package/bundle/hkp-rt/lib/src/services/websocket_server_service.h +196 -0
- package/bundle/hkp-rt/lib/src/services/websocket_writer.cpp +641 -0
- package/bundle/hkp-rt/lib/src/services/websocket_writer.h +117 -0
- package/bundle/hkp-rt/lib/src/sub_runtime.cpp +234 -0
- package/bundle/hkp-rt/lib/src/sub_runtime.h +160 -0
- package/bundle/hkp-rt/lib/src/types/data.cpp +237 -0
- package/bundle/hkp-rt/lib/src/types/message.cpp +148 -0
- package/bundle/hkp-rt/lib/src/types/models.cpp +83 -0
- package/bundle/hkp-rt/lib/src/types/ringbuffer.cpp +336 -0
- package/bundle/hkp-rt/lib/src/uuid.h +12 -0
- package/bundle/readymade-mcp-commits.json +5 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +79 -0
- package/dist/git.d.ts +8 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +53 -0
- package/dist/http.d.ts +17 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +130 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/repository.d.ts +49 -0
- package/dist/repository.d.ts.map +1 -0
- package/dist/repository.js +452 -0
- package/dist/schemas.d.ts +156 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +43 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +83 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +20 -0
- package/dist/validation.d.ts +9 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +101 -0
- 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
|