@hcu-lab.me/mcp-device 1.0.9 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (506) hide show
  1. package/PRIVACY.md +21 -0
  2. package/README.md +58 -3
  3. package/SECURITY.md +67 -0
  4. package/config/revoked-approvals.json +1 -0
  5. package/control-plane/config/mcp-servers.example.toml +52 -0
  6. package/control-plane/config/mcp-servers.toml +52 -0
  7. package/control-plane/contracts/device-public-contract-v1.json +733 -0
  8. package/control-plane/scripts/GATEWAY-LICENSE.txt +7 -0
  9. package/control-plane/scripts/account-http.mjs +220 -0
  10. package/control-plane/scripts/account-store.mjs +368 -0
  11. package/control-plane/scripts/admin.mjs +68 -0
  12. package/control-plane/scripts/auth-session.mjs +411 -0
  13. package/control-plane/scripts/authenticated-mcp-wrapper.mjs +1281 -0
  14. package/control-plane/scripts/benchmark-command.mjs +9 -0
  15. package/control-plane/scripts/benchmark-mcp-catalog.mjs +174 -0
  16. package/control-plane/scripts/catalog-budget.mjs +87 -0
  17. package/control-plane/scripts/check-device-public-contract.mjs +20 -0
  18. package/control-plane/scripts/custom-tools/index.mjs +221 -0
  19. package/control-plane/scripts/custom-tools/path-utils.mjs +117 -0
  20. package/control-plane/scripts/custom-tools/response-utils.mjs +34 -0
  21. package/control-plane/scripts/dashboard-http.mjs +598 -0
  22. package/control-plane/scripts/device-access-policy.mjs +205 -0
  23. package/control-plane/scripts/device-admin.mjs +43 -0
  24. package/control-plane/scripts/device-audit.mjs +63 -0
  25. package/control-plane/scripts/device-broker.mjs +1461 -0
  26. package/control-plane/scripts/device-execution-tool-definitions.mjs +217 -0
  27. package/control-plane/scripts/device-inventory.mjs +128 -0
  28. package/control-plane/scripts/device-pairing-http.mjs +109 -0
  29. package/control-plane/scripts/device-pairing-store.mjs +245 -0
  30. package/control-plane/scripts/device-public-contract.mjs +165 -0
  31. package/control-plane/scripts/device-release.mjs +95 -0
  32. package/control-plane/scripts/device-secure-transport.mjs +128 -0
  33. package/control-plane/scripts/device-store.mjs +283 -0
  34. package/control-plane/scripts/device-usage.mjs +657 -0
  35. package/control-plane/scripts/embedded-device.mjs +149 -0
  36. package/control-plane/scripts/enduser-ui.mjs +45 -0
  37. package/control-plane/scripts/external-tool-broker.mjs +179 -0
  38. package/control-plane/scripts/oauth-authorization-server.mjs +589 -0
  39. package/control-plane/scripts/operator-dashboard-http.mjs +81 -0
  40. package/control-plane/scripts/project-inspection.mjs +227 -0
  41. package/control-plane/scripts/projects/project-id.mjs +69 -0
  42. package/control-plane/scripts/projects/trusted-roots-projects.mjs +375 -0
  43. package/control-plane/scripts/prompts/index.mjs +60 -0
  44. package/control-plane/scripts/remote-process-sessions.mjs +78 -0
  45. package/control-plane/scripts/remote-tool-result.mjs +9 -0
  46. package/control-plane/scripts/resources/index.mjs +166 -0
  47. package/control-plane/scripts/runtime-profile.mjs +54 -0
  48. package/control-plane/scripts/shell-tool-descriptor.mjs +17 -0
  49. package/control-plane/scripts/skill-source-safety.mjs +46 -0
  50. package/control-plane/scripts/skills/README.md +70 -0
  51. package/control-plane/scripts/skills/_upstream_licenses/anthropic-THIRD_PARTY_NOTICES.md +405 -0
  52. package/control-plane/scripts/skills/_upstream_licenses/hallmark-LICENSE.txt +21 -0
  53. package/control-plane/scripts/skills/_upstream_licenses/ponytail-LICENSE.txt +21 -0
  54. package/control-plane/scripts/skills/_upstream_licenses/stitch-skills-LICENSE.txt +201 -0
  55. package/control-plane/scripts/skills/_upstream_licenses/superpowers-LICENSE.txt +21 -0
  56. package/control-plane/scripts/skills/algorithmic-art/LICENSE.txt +202 -0
  57. package/control-plane/scripts/skills/algorithmic-art/SKILL.md +405 -0
  58. package/control-plane/scripts/skills/algorithmic-art/templates/generator_template.js +223 -0
  59. package/control-plane/scripts/skills/algorithmic-art/templates/viewer.html +599 -0
  60. package/control-plane/scripts/skills/brainstorming/SKILL.md +285 -0
  61. package/control-plane/scripts/skills/brainstorming/scripts/frame-template.html +213 -0
  62. package/control-plane/scripts/skills/brainstorming/scripts/helper.js +167 -0
  63. package/control-plane/scripts/skills/brainstorming/scripts/server.cjs +723 -0
  64. package/control-plane/scripts/skills/brainstorming/scripts/start-server.sh +209 -0
  65. package/control-plane/scripts/skills/brainstorming/scripts/stop-server.sh +120 -0
  66. package/control-plane/scripts/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
  67. package/control-plane/scripts/skills/brainstorming/visual-companion.md +299 -0
  68. package/control-plane/scripts/skills/brand-guidelines/LICENSE.txt +202 -0
  69. package/control-plane/scripts/skills/brand-guidelines/SKILL.md +73 -0
  70. package/control-plane/scripts/skills/claude-api/LICENSE.txt +202 -0
  71. package/control-plane/scripts/skills/claude-api/SKILL.md +570 -0
  72. package/control-plane/scripts/skills/claude-api/csharp/claude-api/README.md +362 -0
  73. package/control-plane/scripts/skills/claude-api/csharp/claude-api/batches.md +14 -0
  74. package/control-plane/scripts/skills/claude-api/csharp/claude-api/files-api.md +25 -0
  75. package/control-plane/scripts/skills/claude-api/csharp/claude-api/streaming.md +28 -0
  76. package/control-plane/scripts/skills/claude-api/csharp/claude-api/tool-use.md +164 -0
  77. package/control-plane/scripts/skills/claude-api/curl/examples.md +256 -0
  78. package/control-plane/scripts/skills/claude-api/curl/managed-agents.md +371 -0
  79. package/control-plane/scripts/skills/claude-api/go/claude-api/README.md +188 -0
  80. package/control-plane/scripts/skills/claude-api/go/claude-api/files-api.md +23 -0
  81. package/control-plane/scripts/skills/claude-api/go/claude-api/streaming.md +43 -0
  82. package/control-plane/scripts/skills/claude-api/go/claude-api/tool-use.md +220 -0
  83. package/control-plane/scripts/skills/claude-api/go/managed-agents/README.md +564 -0
  84. package/control-plane/scripts/skills/claude-api/java/claude-api/README.md +238 -0
  85. package/control-plane/scripts/skills/claude-api/java/claude-api/files-api.md +27 -0
  86. package/control-plane/scripts/skills/claude-api/java/claude-api/streaming.md +24 -0
  87. package/control-plane/scripts/skills/claude-api/java/claude-api/tool-use.md +228 -0
  88. package/control-plane/scripts/skills/claude-api/java/managed-agents/README.md +443 -0
  89. package/control-plane/scripts/skills/claude-api/php/claude-api/README.md +174 -0
  90. package/control-plane/scripts/skills/claude-api/php/claude-api/batches.md +15 -0
  91. package/control-plane/scripts/skills/claude-api/php/claude-api/files-api.md +13 -0
  92. package/control-plane/scripts/skills/claude-api/php/claude-api/streaming.md +27 -0
  93. package/control-plane/scripts/skills/claude-api/php/claude-api/tool-use.md +253 -0
  94. package/control-plane/scripts/skills/claude-api/php/managed-agents/README.md +443 -0
  95. package/control-plane/scripts/skills/claude-api/python/claude-api/README.md +565 -0
  96. package/control-plane/scripts/skills/claude-api/python/claude-api/batches.md +198 -0
  97. package/control-plane/scripts/skills/claude-api/python/claude-api/files-api.md +170 -0
  98. package/control-plane/scripts/skills/claude-api/python/claude-api/sdk-upgrade.md +286 -0
  99. package/control-plane/scripts/skills/claude-api/python/claude-api/streaming.md +179 -0
  100. package/control-plane/scripts/skills/claude-api/python/claude-api/tool-use.md +627 -0
  101. package/control-plane/scripts/skills/claude-api/python/managed-agents/README.md +335 -0
  102. package/control-plane/scripts/skills/claude-api/ruby/claude-api/README.md +134 -0
  103. package/control-plane/scripts/skills/claude-api/ruby/claude-api/streaming.md +16 -0
  104. package/control-plane/scripts/skills/claude-api/ruby/claude-api/tool-use.md +41 -0
  105. package/control-plane/scripts/skills/claude-api/ruby/managed-agents/README.md +394 -0
  106. package/control-plane/scripts/skills/claude-api/shared/admin-api.md +179 -0
  107. package/control-plane/scripts/skills/claude-api/shared/agent-design.md +101 -0
  108. package/control-plane/scripts/skills/claude-api/shared/anthropic-cli.md +269 -0
  109. package/control-plane/scripts/skills/claude-api/shared/claude-platform-on-aws.md +59 -0
  110. package/control-plane/scripts/skills/claude-api/shared/cost-optimization.md +233 -0
  111. package/control-plane/scripts/skills/claude-api/shared/error-codes.md +262 -0
  112. package/control-plane/scripts/skills/claude-api/shared/live-sources.md +166 -0
  113. package/control-plane/scripts/skills/claude-api/shared/managed-agents-api-reference.md +455 -0
  114. package/control-plane/scripts/skills/claude-api/shared/managed-agents-client-patterns.md +220 -0
  115. package/control-plane/scripts/skills/claude-api/shared/managed-agents-core.md +359 -0
  116. package/control-plane/scripts/skills/claude-api/shared/managed-agents-environments.md +226 -0
  117. package/control-plane/scripts/skills/claude-api/shared/managed-agents-events.md +290 -0
  118. package/control-plane/scripts/skills/claude-api/shared/managed-agents-memory.md +201 -0
  119. package/control-plane/scripts/skills/claude-api/shared/managed-agents-multiagent.md +260 -0
  120. package/control-plane/scripts/skills/claude-api/shared/managed-agents-onboarding.md +82 -0
  121. package/control-plane/scripts/skills/claude-api/shared/managed-agents-outcomes.md +108 -0
  122. package/control-plane/scripts/skills/claude-api/shared/managed-agents-overview.md +77 -0
  123. package/control-plane/scripts/skills/claude-api/shared/managed-agents-scheduled-deployments.md +156 -0
  124. package/control-plane/scripts/skills/claude-api/shared/managed-agents-self-hosted-sandboxes.md +282 -0
  125. package/control-plane/scripts/skills/claude-api/shared/managed-agents-tools.md +504 -0
  126. package/control-plane/scripts/skills/claude-api/shared/managed-agents-webhooks.md +143 -0
  127. package/control-plane/scripts/skills/claude-api/shared/model-migration.md +1872 -0
  128. package/control-plane/scripts/skills/claude-api/shared/models.md +142 -0
  129. package/control-plane/scripts/skills/claude-api/shared/platform-availability.md +52 -0
  130. package/control-plane/scripts/skills/claude-api/shared/prompt-audit.md +223 -0
  131. package/control-plane/scripts/skills/claude-api/shared/prompt-caching.md +292 -0
  132. package/control-plane/scripts/skills/claude-api/shared/token-counting.md +56 -0
  133. package/control-plane/scripts/skills/claude-api/shared/tool-use-concepts.md +526 -0
  134. package/control-plane/scripts/skills/claude-api/typescript/claude-api/README.md +405 -0
  135. package/control-plane/scripts/skills/claude-api/typescript/claude-api/batches.md +106 -0
  136. package/control-plane/scripts/skills/claude-api/typescript/claude-api/files-api.md +98 -0
  137. package/control-plane/scripts/skills/claude-api/typescript/claude-api/streaming.md +178 -0
  138. package/control-plane/scripts/skills/claude-api/typescript/claude-api/tool-use.md +586 -0
  139. package/control-plane/scripts/skills/claude-api/typescript/managed-agents/README.md +359 -0
  140. package/control-plane/scripts/skills/dispatching-parallel-agents/SKILL.md +167 -0
  141. package/control-plane/scripts/skills/enhance-prompt/README.md +34 -0
  142. package/control-plane/scripts/skills/enhance-prompt/SKILL.md +204 -0
  143. package/control-plane/scripts/skills/enhance-prompt/references/KEYWORDS.md +114 -0
  144. package/control-plane/scripts/skills/executing-plans/SKILL.md +371 -0
  145. package/control-plane/scripts/skills/executing-plans/scripts/task-done +52 -0
  146. package/control-plane/scripts/skills/executing-plans/scripts/task-start +28 -0
  147. package/control-plane/scripts/skills/extract-design-md/SKILL.md +358 -0
  148. package/control-plane/scripts/skills/extract-design-md/examples/DESIGN.md +210 -0
  149. package/control-plane/scripts/skills/extract-design-md/references/angular.md +122 -0
  150. package/control-plane/scripts/skills/extract-design-md/references/plain-css.md +161 -0
  151. package/control-plane/scripts/skills/extract-design-md/references/react-tailwind.md +157 -0
  152. package/control-plane/scripts/skills/extract-design-md/references/svelte.md +101 -0
  153. package/control-plane/scripts/skills/extract-design-md/references/vue.md +107 -0
  154. package/control-plane/scripts/skills/finishing-a-development-branch/SKILL.md +225 -0
  155. package/control-plane/scripts/skills/frontend-design/LICENSE.txt +177 -0
  156. package/control-plane/scripts/skills/frontend-design/SKILL.md +71 -0
  157. package/control-plane/scripts/skills/hallmark/SKILL.md +558 -0
  158. package/control-plane/scripts/skills/hallmark/references/anti-patterns.md +418 -0
  159. package/control-plane/scripts/skills/hallmark/references/assets.md +406 -0
  160. package/control-plane/scripts/skills/hallmark/references/color.md +95 -0
  161. package/control-plane/scripts/skills/hallmark/references/component-cookbook.md +263 -0
  162. package/control-plane/scripts/skills/hallmark/references/components/c1-outlined-chip.md +12 -0
  163. package/control-plane/scripts/skills/hallmark/references/components/c2-inline-form-as-cta.md +16 -0
  164. package/control-plane/scripts/skills/hallmark/references/components/c3-typographic-link.md +8 -0
  165. package/control-plane/scripts/skills/hallmark/references/components/c4-sticky-bottom-bar.md +16 -0
  166. package/control-plane/scripts/skills/hallmark/references/components/f1-bento-grid.md +20 -0
  167. package/control-plane/scripts/skills/hallmark/references/components/f2-sticky-scroll-stack.md +20 -0
  168. package/control-plane/scripts/skills/hallmark/references/components/f3-tabular-spec-sheet.md +11 -0
  169. package/control-plane/scripts/skills/hallmark/references/components/f4-step-sequence.md +11 -0
  170. package/control-plane/scripts/skills/hallmark/references/components/f5-annotated-screenshot.md +11 -0
  171. package/control-plane/scripts/skills/hallmark/references/components/f6-product-card-grid.md +41 -0
  172. package/control-plane/scripts/skills/hallmark/references/components/ft1-mast-headed.md +13 -0
  173. package/control-plane/scripts/skills/hallmark/references/components/ft2-inline-rule-single-line.md +10 -0
  174. package/control-plane/scripts/skills/hallmark/references/components/ft3-index-style-category-list.md +12 -0
  175. package/control-plane/scripts/skills/hallmark/references/components/ft4-dense-typographic.md +10 -0
  176. package/control-plane/scripts/skills/hallmark/references/components/ft5-statement.md +21 -0
  177. package/control-plane/scripts/skills/hallmark/references/components/ft6-letter-close.md +19 -0
  178. package/control-plane/scripts/skills/hallmark/references/components/ft7-newsletter-first.md +27 -0
  179. package/control-plane/scripts/skills/hallmark/references/components/ft8-marquee-scroll.md +25 -0
  180. package/control-plane/scripts/skills/hallmark/references/components/h1-marquee.md +15 -0
  181. package/control-plane/scripts/skills/hallmark/references/components/h2-split-diptych.md +15 -0
  182. package/control-plane/scripts/skills/hallmark/references/components/h3-quote-led.md +11 -0
  183. package/control-plane/scripts/skills/hallmark/references/components/h4-stat-led.md +14 -0
  184. package/control-plane/scripts/skills/hallmark/references/components/h5-letter-hero.md +11 -0
  185. package/control-plane/scripts/skills/hallmark/references/components/h6-photographic-fold.md +16 -0
  186. package/control-plane/scripts/skills/hallmark/references/components/h7-demo-video-clipped-by-viewport-edge.md +27 -0
  187. package/control-plane/scripts/skills/hallmark/references/components/h8-mockup-split-browser-framed.md +23 -0
  188. package/control-plane/scripts/skills/hallmark/references/components/h9-custom-illustration-centerpiece.md +27 -0
  189. package/control-plane/scripts/skills/hallmark/references/components/n1-wordmark-2-links.md +12 -0
  190. package/control-plane/scripts/skills/hallmark/references/components/n10-floating-on-scroll-morph.md +19 -0
  191. package/control-plane/scripts/skills/hallmark/references/components/n11-mega-menu.md +40 -0
  192. package/control-plane/scripts/skills/hallmark/references/components/n12-banner-retract.md +34 -0
  193. package/control-plane/scripts/skills/hallmark/references/components/n13-inline-cmdk-pill.md +39 -0
  194. package/control-plane/scripts/skills/hallmark/references/components/n1b-saas-three-section.md +35 -0
  195. package/control-plane/scripts/skills/hallmark/references/components/n2-floating-chip.md +14 -0
  196. package/control-plane/scripts/skills/hallmark/references/components/n3-side-rail.md +14 -0
  197. package/control-plane/scripts/skills/hallmark/references/components/n4-hidden-behind-k.md +9 -0
  198. package/control-plane/scripts/skills/hallmark/references/components/n5-floating-pill.md +28 -0
  199. package/control-plane/scripts/skills/hallmark/references/components/n6-newspaper-masthead.md +24 -0
  200. package/control-plane/scripts/skills/hallmark/references/components/n7-brutal-slab.md +22 -0
  201. package/control-plane/scripts/skills/hallmark/references/components/n8-terminal-command.md +21 -0
  202. package/control-plane/scripts/skills/hallmark/references/components/n9-edge-aligned-minimal.md +17 -0
  203. package/control-plane/scripts/skills/hallmark/references/components/s1-left-margin-numbered.md +15 -0
  204. package/control-plane/scripts/skills/hallmark/references/components/s2-hanging.md +13 -0
  205. package/control-plane/scripts/skills/hallmark/references/components/s3-sticky-pinned.md +19 -0
  206. package/control-plane/scripts/skills/hallmark/references/components/s4-inline-no-break.md +11 -0
  207. package/control-plane/scripts/skills/hallmark/references/components/s5-bottom-anchored.md +13 -0
  208. package/control-plane/scripts/skills/hallmark/references/components/t1-pull-quote-with-marginalia.md +12 -0
  209. package/control-plane/scripts/skills/hallmark/references/components/t2-logo-wall-hairline.md +19 -0
  210. package/control-plane/scripts/skills/hallmark/references/components/t3-single-huge-quote.md +11 -0
  211. package/control-plane/scripts/skills/hallmark/references/components/t4-numbered-stat-strip.md +14 -0
  212. package/control-plane/scripts/skills/hallmark/references/contract.md +25 -0
  213. package/control-plane/scripts/skills/hallmark/references/copy.md +182 -0
  214. package/control-plane/scripts/skills/hallmark/references/custom-craft.md +626 -0
  215. package/control-plane/scripts/skills/hallmark/references/custom-theme.md +367 -0
  216. package/control-plane/scripts/skills/hallmark/references/design-md.md +116 -0
  217. package/control-plane/scripts/skills/hallmark/references/export-formats.md +329 -0
  218. package/control-plane/scripts/skills/hallmark/references/floating-nav.md +89 -0
  219. package/control-plane/scripts/skills/hallmark/references/genres/atmospheric.md +69 -0
  220. package/control-plane/scripts/skills/hallmark/references/genres/editorial.md +72 -0
  221. package/control-plane/scripts/skills/hallmark/references/genres/modern-minimal.md +71 -0
  222. package/control-plane/scripts/skills/hallmark/references/genres/playful.md +70 -0
  223. package/control-plane/scripts/skills/hallmark/references/hero-enrichment.md +475 -0
  224. package/control-plane/scripts/skills/hallmark/references/imagery-kit.md +170 -0
  225. package/control-plane/scripts/skills/hallmark/references/interaction-and-states.md +208 -0
  226. package/control-plane/scripts/skills/hallmark/references/layout-and-space.md +112 -0
  227. package/control-plane/scripts/skills/hallmark/references/macrostructures/01-bento-grid.md +35 -0
  228. package/control-plane/scripts/skills/hallmark/references/macrostructures/02-long-document.md +34 -0
  229. package/control-plane/scripts/skills/hallmark/references/macrostructures/03-marquee-hero.md +31 -0
  230. package/control-plane/scripts/skills/hallmark/references/macrostructures/04-stat-led.md +34 -0
  231. package/control-plane/scripts/skills/hallmark/references/macrostructures/05-workbench.md +32 -0
  232. package/control-plane/scripts/skills/hallmark/references/macrostructures/06-conversational-faq.md +33 -0
  233. package/control-plane/scripts/skills/hallmark/references/macrostructures/07-manifesto.md +32 -0
  234. package/control-plane/scripts/skills/hallmark/references/macrostructures/08-photographic.md +34 -0
  235. package/control-plane/scripts/skills/hallmark/references/macrostructures/09-quote-led.md +32 -0
  236. package/control-plane/scripts/skills/hallmark/references/macrostructures/10-specimen.md +32 -0
  237. package/control-plane/scripts/skills/hallmark/references/macrostructures/11-catalogue.md +23 -0
  238. package/control-plane/scripts/skills/hallmark/references/macrostructures/12-letter.md +23 -0
  239. package/control-plane/scripts/skills/hallmark/references/macrostructures/13-index-first.md +23 -0
  240. package/control-plane/scripts/skills/hallmark/references/macrostructures/14-narrative-workflow.md +23 -0
  241. package/control-plane/scripts/skills/hallmark/references/macrostructures/15-split-studio.md +23 -0
  242. package/control-plane/scripts/skills/hallmark/references/macrostructures/16-feature-stack.md +23 -0
  243. package/control-plane/scripts/skills/hallmark/references/macrostructures/17-type-specimen.md +23 -0
  244. package/control-plane/scripts/skills/hallmark/references/macrostructures/18-portfolio-grid.md +23 -0
  245. package/control-plane/scripts/skills/hallmark/references/macrostructures/19-map-diagram.md +23 -0
  246. package/control-plane/scripts/skills/hallmark/references/macrostructures/20-ecosystem-index.md +23 -0
  247. package/control-plane/scripts/skills/hallmark/references/macrostructures/21-component-playground.md +23 -0
  248. package/control-plane/scripts/skills/hallmark/references/macrostructures.md +89 -0
  249. package/control-plane/scripts/skills/hallmark/references/microinteractions.md +258 -0
  250. package/control-plane/scripts/skills/hallmark/references/motion.md +109 -0
  251. package/control-plane/scripts/skills/hallmark/references/preview-examples.md +49 -0
  252. package/control-plane/scripts/skills/hallmark/references/responsive.md +138 -0
  253. package/control-plane/scripts/skills/hallmark/references/slop-test.md +192 -0
  254. package/control-plane/scripts/skills/hallmark/references/structure.md +160 -0
  255. package/control-plane/scripts/skills/hallmark/references/study.md +509 -0
  256. package/control-plane/scripts/skills/hallmark/references/themes/carnival.md +301 -0
  257. package/control-plane/scripts/skills/hallmark/references/themes/cobalt.md +146 -0
  258. package/control-plane/scripts/skills/hallmark/references/themes/grid.md +161 -0
  259. package/control-plane/scripts/skills/hallmark/references/themes/hum.md +403 -0
  260. package/control-plane/scripts/skills/hallmark/references/themes/lumen.md +478 -0
  261. package/control-plane/scripts/skills/hallmark/references/typography.md +243 -0
  262. package/control-plane/scripts/skills/hallmark/references/upstream/recipes.md +186 -0
  263. package/control-plane/scripts/skills/hallmark/references/upstream/study-examples.md +176 -0
  264. package/control-plane/scripts/skills/hallmark/references/upstream/tokens.css +1221 -0
  265. package/control-plane/scripts/skills/hallmark/references/verbs/audit.md +25 -0
  266. package/control-plane/scripts/skills/hallmark/references/verbs/redesign.md +269 -0
  267. package/control-plane/scripts/skills/human-comms/SKILL.md +29 -0
  268. package/control-plane/scripts/skills/index.mjs +432 -0
  269. package/control-plane/scripts/skills/internal-comms/LICENSE.txt +202 -0
  270. package/control-plane/scripts/skills/internal-comms/SKILL.md +32 -0
  271. package/control-plane/scripts/skills/internal-comms/examples/3p-updates.md +47 -0
  272. package/control-plane/scripts/skills/internal-comms/examples/company-newsletter.md +65 -0
  273. package/control-plane/scripts/skills/internal-comms/examples/faq-answers.md +30 -0
  274. package/control-plane/scripts/skills/internal-comms/examples/general-comms.md +16 -0
  275. package/control-plane/scripts/skills/mcp-adapter.mjs +94 -0
  276. package/control-plane/scripts/skills/mcp-builder/LICENSE.txt +202 -0
  277. package/control-plane/scripts/skills/mcp-builder/SKILL.md +236 -0
  278. package/control-plane/scripts/skills/mcp-builder/reference/evaluation.md +602 -0
  279. package/control-plane/scripts/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
  280. package/control-plane/scripts/skills/mcp-builder/reference/node_mcp_server.md +970 -0
  281. package/control-plane/scripts/skills/mcp-builder/reference/python_mcp_server.md +719 -0
  282. package/control-plane/scripts/skills/mcp-builder/scripts/connections.py +151 -0
  283. package/control-plane/scripts/skills/mcp-builder/scripts/evaluation.py +373 -0
  284. package/control-plane/scripts/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  285. package/control-plane/scripts/skills/mcp-builder/scripts/requirements.txt +2 -0
  286. package/control-plane/scripts/skills/ponytail/SKILL.md +120 -0
  287. package/control-plane/scripts/skills/ponytail-audit/SKILL.md +41 -0
  288. package/control-plane/scripts/skills/ponytail-debt/SKILL.md +44 -0
  289. package/control-plane/scripts/skills/ponytail-gain/SKILL.md +50 -0
  290. package/control-plane/scripts/skills/ponytail-help/SKILL.md +71 -0
  291. package/control-plane/scripts/skills/ponytail-review/SKILL.md +57 -0
  292. package/control-plane/scripts/skills/receiving-code-review/SKILL.md +205 -0
  293. package/control-plane/scripts/skills/requesting-code-review/SKILL.md +95 -0
  294. package/control-plane/scripts/skills/requesting-code-review/code-reviewer.md +198 -0
  295. package/control-plane/scripts/skills/skill-creator/LICENSE.txt +202 -0
  296. package/control-plane/scripts/skills/skill-creator/SKILL.md +485 -0
  297. package/control-plane/scripts/skills/skill-creator/agents/analyzer.md +274 -0
  298. package/control-plane/scripts/skills/skill-creator/agents/comparator.md +202 -0
  299. package/control-plane/scripts/skills/skill-creator/agents/grader.md +223 -0
  300. package/control-plane/scripts/skills/skill-creator/assets/eval_review.html +146 -0
  301. package/control-plane/scripts/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  302. package/control-plane/scripts/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  303. package/control-plane/scripts/skills/skill-creator/references/schemas.md +430 -0
  304. package/control-plane/scripts/skills/skill-creator/scripts/__init__.py +0 -0
  305. package/control-plane/scripts/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  306. package/control-plane/scripts/skills/skill-creator/scripts/generate_report.py +326 -0
  307. package/control-plane/scripts/skills/skill-creator/scripts/improve_description.py +247 -0
  308. package/control-plane/scripts/skills/skill-creator/scripts/package_skill.py +136 -0
  309. package/control-plane/scripts/skills/skill-creator/scripts/quick_validate.py +103 -0
  310. package/control-plane/scripts/skills/skill-creator/scripts/run_eval.py +310 -0
  311. package/control-plane/scripts/skills/skill-creator/scripts/run_loop.py +328 -0
  312. package/control-plane/scripts/skills/skill-creator/scripts/utils.py +47 -0
  313. package/control-plane/scripts/skills/slack-gif-creator/LICENSE.txt +202 -0
  314. package/control-plane/scripts/skills/slack-gif-creator/SKILL.md +254 -0
  315. package/control-plane/scripts/skills/slack-gif-creator/core/easing.py +234 -0
  316. package/control-plane/scripts/skills/slack-gif-creator/core/frame_composer.py +176 -0
  317. package/control-plane/scripts/skills/slack-gif-creator/core/gif_builder.py +269 -0
  318. package/control-plane/scripts/skills/slack-gif-creator/core/validators.py +136 -0
  319. package/control-plane/scripts/skills/slack-gif-creator/requirements.txt +4 -0
  320. package/control-plane/scripts/skills/sources.json +401 -0
  321. package/control-plane/scripts/skills/sources.lock.json +416 -0
  322. package/control-plane/scripts/skills/subagent-driven-development/SKILL.md +570 -0
  323. package/control-plane/scripts/skills/subagent-driven-development/implementer-prompt.md +154 -0
  324. package/control-plane/scripts/skills/subagent-driven-development/re-review-prompt.md +115 -0
  325. package/control-plane/scripts/skills/subagent-driven-development/scripts/review-package +53 -0
  326. package/control-plane/scripts/skills/subagent-driven-development/scripts/sdd-workspace +82 -0
  327. package/control-plane/scripts/skills/subagent-driven-development/scripts/task-brief +43 -0
  328. package/control-plane/scripts/skills/subagent-driven-development/task-reviewer-prompt.md +207 -0
  329. package/control-plane/scripts/skills/systematic-debugging/CREATION-LOG.md +119 -0
  330. package/control-plane/scripts/skills/systematic-debugging/SKILL.md +283 -0
  331. package/control-plane/scripts/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  332. package/control-plane/scripts/skills/systematic-debugging/condition-based-waiting.md +115 -0
  333. package/control-plane/scripts/skills/systematic-debugging/defense-in-depth.md +122 -0
  334. package/control-plane/scripts/skills/systematic-debugging/find-polluter.sh +72 -0
  335. package/control-plane/scripts/skills/systematic-debugging/root-cause-tracing.md +169 -0
  336. package/control-plane/scripts/skills/systematic-debugging/test-academic.md +14 -0
  337. package/control-plane/scripts/skills/systematic-debugging/test-pressure-1.md +58 -0
  338. package/control-plane/scripts/skills/systematic-debugging/test-pressure-2.md +68 -0
  339. package/control-plane/scripts/skills/systematic-debugging/test-pressure-3.md +69 -0
  340. package/control-plane/scripts/skills/test-driven-development/SKILL.md +330 -0
  341. package/control-plane/scripts/skills/test-driven-development/writing-good-tests.md +198 -0
  342. package/control-plane/scripts/skills/theme-factory/LICENSE.txt +202 -0
  343. package/control-plane/scripts/skills/theme-factory/SKILL.md +59 -0
  344. package/control-plane/scripts/skills/theme-factory/theme-showcase.pdf +0 -0
  345. package/control-plane/scripts/skills/theme-factory/themes/arctic-frost.md +19 -0
  346. package/control-plane/scripts/skills/theme-factory/themes/botanical-garden.md +19 -0
  347. package/control-plane/scripts/skills/theme-factory/themes/desert-rose.md +19 -0
  348. package/control-plane/scripts/skills/theme-factory/themes/forest-canopy.md +19 -0
  349. package/control-plane/scripts/skills/theme-factory/themes/golden-hour.md +19 -0
  350. package/control-plane/scripts/skills/theme-factory/themes/midnight-galaxy.md +19 -0
  351. package/control-plane/scripts/skills/theme-factory/themes/modern-minimalist.md +19 -0
  352. package/control-plane/scripts/skills/theme-factory/themes/ocean-depths.md +19 -0
  353. package/control-plane/scripts/skills/theme-factory/themes/sunset-boulevard.md +19 -0
  354. package/control-plane/scripts/skills/theme-factory/themes/tech-innovation.md +19 -0
  355. package/control-plane/scripts/skills/using-git-worktrees/SKILL.md +167 -0
  356. package/control-plane/scripts/skills/verification-before-completion/SKILL.md +120 -0
  357. package/control-plane/scripts/skills/web-artifacts-builder/LICENSE.txt +202 -0
  358. package/control-plane/scripts/skills/web-artifacts-builder/SKILL.md +74 -0
  359. package/control-plane/scripts/skills/web-artifacts-builder/scripts/bundle-artifact.sh +54 -0
  360. package/control-plane/scripts/skills/web-artifacts-builder/scripts/init-artifact.sh +322 -0
  361. package/control-plane/scripts/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  362. package/control-plane/scripts/skills/webapp-testing/LICENSE.txt +202 -0
  363. package/control-plane/scripts/skills/webapp-testing/SKILL.md +96 -0
  364. package/control-plane/scripts/skills/webapp-testing/examples/console_logging.py +35 -0
  365. package/control-plane/scripts/skills/webapp-testing/examples/element_discovery.py +40 -0
  366. package/control-plane/scripts/skills/webapp-testing/examples/static_html_automation.py +33 -0
  367. package/control-plane/scripts/skills/webapp-testing/scripts/with_server.py +106 -0
  368. package/control-plane/scripts/skills/writing-plans/SKILL.md +192 -0
  369. package/control-plane/scripts/skills/writing-plans/plan-document-reviewer-prompt.md +49 -0
  370. package/control-plane/scripts/skills/writing-skills/SKILL.md +681 -0
  371. package/control-plane/scripts/skills/writing-skills/anthropic-best-practices.md +1150 -0
  372. package/control-plane/scripts/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
  373. package/control-plane/scripts/skills/writing-skills/graphviz-conventions.dot +172 -0
  374. package/control-plane/scripts/skills/writing-skills/persuasion-principles.md +187 -0
  375. package/control-plane/scripts/skills/writing-skills/render-graphs.js +169 -0
  376. package/control-plane/scripts/skills/writing-skills/testing-skills-with-subagents.md +384 -0
  377. package/control-plane/scripts/smoke-mcp-schemas.mjs +30 -0
  378. package/control-plane/scripts/surface-config.mjs +45 -0
  379. package/control-plane/scripts/sync-skills.mjs +370 -0
  380. package/control-plane/scripts/tool-metrics.mjs +74 -0
  381. package/control-plane/scripts/tool-risk.mjs +156 -0
  382. package/control-plane/scripts/tool-surface-stability.mjs +38 -0
  383. package/control-plane/scripts/upstreams/catalog-cache.mjs +28 -0
  384. package/control-plane/scripts/upstreams/config.mjs +158 -0
  385. package/control-plane/scripts/upstreams/diagnostics.mjs +21 -0
  386. package/control-plane/scripts/upstreams/http-client.mjs +67 -0
  387. package/control-plane/scripts/upstreams/manager.mjs +535 -0
  388. package/control-plane/scripts/upstreams/names.mjs +55 -0
  389. package/control-plane/scripts/upstreams/presets.mjs +60 -0
  390. package/control-plane/scripts/upstreams/resource-uri.mjs +29 -0
  391. package/control-plane/scripts/upstreams/stdio-client.mjs +73 -0
  392. package/control-plane/scripts/workspace-registry.mjs +147 -0
  393. package/dist/context/authorization.d.ts +5 -0
  394. package/dist/context/authorization.js +70 -0
  395. package/dist/context/capture.d.ts +46 -0
  396. package/dist/context/capture.js +187 -0
  397. package/dist/context/cli.d.ts +47 -0
  398. package/dist/context/cli.js +287 -0
  399. package/dist/context/device-adapter.d.ts +102 -0
  400. package/dist/context/device-adapter.js +79 -0
  401. package/dist/context/gateway.d.ts +354 -0
  402. package/dist/context/gateway.js +50 -0
  403. package/dist/context/graph.d.ts +63 -0
  404. package/dist/context/graph.js +214 -0
  405. package/dist/context/importer.d.ts +8 -0
  406. package/dist/context/importer.js +69 -0
  407. package/dist/context/indexer.d.ts +14 -0
  408. package/dist/context/indexer.js +57 -0
  409. package/dist/context/migrations/index.d.ts +4 -0
  410. package/dist/context/migrations/index.js +87 -0
  411. package/dist/context/redact.d.ts +15 -0
  412. package/dist/context/redact.js +43 -0
  413. package/dist/context/repositories.d.ts +9 -0
  414. package/dist/context/repositories.js +45 -0
  415. package/dist/context/search.d.ts +63 -0
  416. package/dist/context/search.js +207 -0
  417. package/dist/context/service.d.ts +278 -0
  418. package/dist/context/service.js +160 -0
  419. package/dist/context/store.d.ts +134 -0
  420. package/dist/context/store.js +722 -0
  421. package/dist/context/text.d.ts +4 -0
  422. package/dist/context/text.js +14 -0
  423. package/dist/context/tool-contract.d.ts +534 -0
  424. package/dist/context/tool-contract.js +45 -0
  425. package/dist/device/core.d.ts +29 -0
  426. package/dist/device/core.js +66 -0
  427. package/dist/device/device.d.ts +5 -0
  428. package/dist/device/device.js +94 -37
  429. package/dist/device/gateway-channel.d.ts +8 -1
  430. package/dist/device/gateway-channel.js +51 -3
  431. package/dist/device/gateway-tool-adapter.d.ts +11 -3
  432. package/dist/device/gateway-tool-adapter.js +141 -98
  433. package/dist/device/official-trust.d.ts +12 -0
  434. package/dist/device/official-trust.js +25 -1
  435. package/dist/device/runtime-owner.js +9 -1
  436. package/dist/distribution.d.ts +4 -0
  437. package/dist/distribution.js +8 -0
  438. package/dist/handlers/filesystem-handlers.js +53 -37
  439. package/dist/handlers/search-handlers.d.ts +1 -0
  440. package/dist/handlers/search-handlers.js +51 -1
  441. package/dist/log-palace/cli.d.ts +7 -0
  442. package/dist/log-palace/cli.js +89 -0
  443. package/dist/log-palace/formats.d.ts +26 -0
  444. package/dist/log-palace/formats.js +126 -0
  445. package/dist/log-palace/store.d.ts +129 -0
  446. package/dist/log-palace/store.js +856 -0
  447. package/dist/mcp-device.d.ts +1 -1
  448. package/dist/mcp-device.js +28 -6
  449. package/dist/npm-scripts/remote.js +1 -0
  450. package/dist/platform/activity-journal.d.ts +16 -0
  451. package/dist/platform/activity-journal.js +68 -0
  452. package/dist/platform/cli.d.ts +1 -0
  453. package/dist/platform/cli.js +300 -0
  454. package/dist/platform/coordination-lock.d.ts +45 -0
  455. package/dist/platform/coordination-lock.js +233 -0
  456. package/dist/platform/device-runtime.d.ts +16 -0
  457. package/dist/platform/device-runtime.js +83 -0
  458. package/dist/platform/knowledge-contract.d.ts +46 -0
  459. package/dist/platform/knowledge-contract.js +1 -0
  460. package/dist/platform/knowledge-mcp.d.ts +29 -0
  461. package/dist/platform/knowledge-mcp.js +95 -0
  462. package/dist/platform/knowledge-router.d.ts +64 -0
  463. package/dist/platform/knowledge-router.js +320 -0
  464. package/dist/platform/plugin-child.d.ts +1 -0
  465. package/dist/platform/plugin-child.js +115 -0
  466. package/dist/platform/plugin-contract.d.ts +76 -0
  467. package/dist/platform/plugin-contract.js +53 -0
  468. package/dist/platform/plugin-driver.d.ts +8 -0
  469. package/dist/platform/plugin-driver.js +443 -0
  470. package/dist/platform/plugin-host.d.ts +88 -0
  471. package/dist/platform/plugin-host.js +817 -0
  472. package/dist/platform/plugin-policy.d.ts +3 -0
  473. package/dist/platform/plugin-policy.js +29 -0
  474. package/dist/platform/plugin-services.d.ts +14 -0
  475. package/dist/platform/plugin-services.js +32 -0
  476. package/dist/platform/process-identity.d.ts +9 -0
  477. package/dist/platform/process-identity.js +68 -0
  478. package/dist/platform/runtime-lease.d.ts +4 -0
  479. package/dist/platform/runtime-lease.js +58 -0
  480. package/dist/platform/runtime-root.d.ts +9 -0
  481. package/dist/platform/runtime-root.js +61 -0
  482. package/dist/platform/state-backup.d.ts +92 -0
  483. package/dist/platform/state-backup.js +399 -0
  484. package/dist/plugins/reference/plugin.d.ts +3 -0
  485. package/dist/plugins/reference/plugin.js +13 -0
  486. package/dist/search-manager.d.ts +6 -0
  487. package/dist/search-manager.js +123 -0
  488. package/dist/server.d.ts +13 -0
  489. package/dist/server.js +7 -3
  490. package/dist/terminal-manager.d.ts +16 -0
  491. package/dist/terminal-manager.js +137 -42
  492. package/dist/tool-dispatcher.d.ts +11 -0
  493. package/dist/tool-dispatcher.js +228 -0
  494. package/dist/tools/edit.js +72 -1
  495. package/dist/tools/filesystem.d.ts +1 -1
  496. package/dist/tools/filesystem.js +2 -2
  497. package/dist/tools/improved-process-tools.js +91 -108
  498. package/dist/tools/schemas.d.ts +94 -2
  499. package/dist/tools/schemas.js +20 -6
  500. package/dist/types.d.ts +8 -0
  501. package/dist/utils/output-budget.d.ts +6 -0
  502. package/dist/utils/output-budget.js +15 -0
  503. package/dist/version.d.ts +1 -1
  504. package/dist/version.js +1 -1
  505. package/package.json +29 -6
  506. package/skills/mcp-device-context/SKILL.md +40 -0
@@ -0,0 +1,1461 @@
1
+ import { createHash, createPublicKey, randomBytes, randomUUID, timingSafeEqual, verify } from 'node:crypto';
2
+ import { WebSocket, WebSocketServer } from 'ws';
3
+ import {
4
+ DEVICE_INNER_TLS_SUBPROTOCOL,
5
+ createJsonFrameParser,
6
+ createServerInnerTls,
7
+ encodeJsonFrame
8
+ } from './device-secure-transport.mjs';
9
+ import { compareStableVersions, minimumSelfUpdateVersion, normalizeStableVersion, supportsDeviceSelfUpdate } from './device-release.mjs';
10
+
11
+ export const DEVICE_PROTOCOL_VERSION = 1;
12
+ export const DEVICE_PROTOCOL_VERSION_V2 = 2;
13
+ const DEVICE_AUTH_EXPORTER_LABEL = 'EXPERIMENTAL-HCU-MCP-DEVICE-AUTH-V2';
14
+ const DEFAULT_REQUEST_TIMEOUT_MS = 10000;
15
+ const DEFAULT_HELLO_TIMEOUT_MS = 5000;
16
+ const DEFAULT_UPDATE_TIMEOUT_MS = 15 * 60 * 1000;
17
+ const MAX_MESSAGE_BYTES = 64 * 1024;
18
+ const DEVICE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
19
+
20
+ export function buildDeviceAuthChallenge({ deviceId, nonce }) {
21
+ return Buffer.from(`mcp-device-auth-v1\n${deviceId}\n${nonce}`, 'utf8');
22
+ }
23
+
24
+ const DEVICE_AUTH_V2_MODE = Object.freeze({ reconnect: 0, enroll: 1, pair: 2 });
25
+
26
+ function fixed32(value, name) {
27
+ const bytes = Buffer.isBuffer(value) ? value : Buffer.from(String(value || ''), 'base64url');
28
+ if (bytes.length !== 32) throw new Error(`${name} must be exactly 32 bytes.`);
29
+ return bytes;
30
+ }
31
+
32
+ export function buildDeviceAuthExporterContext({ deviceId, mode }) {
33
+ if (!DEVICE_ID_PATTERN.test(String(deviceId || ''))) throw new Error('Invalid device_id.');
34
+ if (!(mode in DEVICE_AUTH_V2_MODE)) throw new Error('Invalid device authentication mode.');
35
+ return createHash('sha256').update(`hcu-mcp-device-auth-v2\n${deviceId}\n${mode}`, 'utf8').digest();
36
+ }
37
+
38
+ export function buildDeviceAuthChallengeV2({ deviceId, mode, nonce, exporter, publicKeyPem, grant = null }) {
39
+ const deviceBytes = Buffer.from(String(deviceId || ''), 'utf8');
40
+ if (!DEVICE_ID_PATTERN.test(String(deviceId || '')) || deviceBytes.length > 0xffff) throw new Error('Invalid device_id.');
41
+ if (!(mode in DEVICE_AUTH_V2_MODE)) throw new Error('Invalid device authentication mode.');
42
+ const publicKey = createPublicKey(publicKeyPem);
43
+ if (publicKey.asymmetricKeyType !== 'ed25519') throw new Error('Device public key must be Ed25519.');
44
+ const publicKeyDer = publicKey.export({ type: 'spki', format: 'der' });
45
+ const grantDigest = mode === 'reconnect'
46
+ ? Buffer.alloc(32)
47
+ : createHash('sha256').update(String(grant || ''), 'utf8').digest();
48
+ const deviceLength = Buffer.allocUnsafe(2);
49
+ deviceLength.writeUInt16BE(deviceBytes.length, 0);
50
+ return Buffer.concat([
51
+ Buffer.from('hcu-mcp-device-auth-v2\0', 'ascii'),
52
+ Buffer.from([2, DEVICE_AUTH_V2_MODE[mode]]),
53
+ deviceLength,
54
+ deviceBytes,
55
+ fixed32(nonce, 'gateway nonce'),
56
+ fixed32(exporter, 'TLS exporter'),
57
+ createHash('sha256').update(publicKeyDer).digest(),
58
+ grantDigest
59
+ ]);
60
+ }
61
+
62
+ function bearerToken(request) {
63
+ const value = String(request.headers.authorization || '');
64
+ return value.startsWith('Bearer ') ? value.slice('Bearer '.length) : '';
65
+ }
66
+
67
+ function tokenMatches(actual, expected) {
68
+ if (!actual || !expected) return false;
69
+ const left = Buffer.from(actual, 'utf8');
70
+ const right = Buffer.from(expected, 'utf8');
71
+ return left.length === right.length && timingSafeEqual(left, right);
72
+ }
73
+
74
+ function rejectUpgrade(socket, statusCode, message) {
75
+ if (!socket.writable) return socket.destroy();
76
+ const body = `${message}\n`;
77
+ socket.write(`HTTP/1.1 ${statusCode} ${message}\r\nConnection: close\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
78
+ socket.destroy();
79
+ }
80
+
81
+ function validateMessage(message, expectedVersion) {
82
+ if (!message || typeof message !== 'object') throw new Error('Device message must be an object.');
83
+ if (message.protocol_version !== expectedVersion) throw new Error('Unsupported device protocol version.');
84
+ if (typeof message.type !== 'string' || !message.type) throw new Error('Device message type is required.');
85
+ return message;
86
+ }
87
+
88
+ function parseMessage(raw, expectedVersion = DEVICE_PROTOCOL_VERSION) {
89
+ const text = Buffer.isBuffer(raw) ? raw.toString('utf8') : String(raw);
90
+ if (Buffer.byteLength(text, 'utf8') > MAX_MESSAGE_BYTES) throw new Error('Device message exceeds maximum size.');
91
+ return validateMessage(JSON.parse(text), expectedVersion);
92
+ }
93
+
94
+ function normalizeDeviceId(value) {
95
+ const deviceId = String(value || '').trim();
96
+ if (!DEVICE_ID_PATTERN.test(deviceId)) throw new Error('Invalid device_id.');
97
+ return deviceId;
98
+ }
99
+
100
+ function normalizeCapabilities(value) {
101
+ if (!Array.isArray(value)) return [];
102
+ return [...new Set(value.map(item => String(item).trim()).filter(Boolean))].sort();
103
+ }
104
+
105
+ function machineMetadata(payload = {}) {
106
+ const pathStyle = String(payload.path_style || '').trim();
107
+ return {
108
+ hostname: String(payload.hostname || '').trim().slice(0, 128) || null,
109
+ platform: String(payload.platform || '').trim().slice(0, 32) || null,
110
+ arch: String(payload.arch || '').trim().slice(0, 32) || null,
111
+ pathStyle: ['windows', 'posix'].includes(pathStyle) ? pathStyle : null
112
+ };
113
+ }
114
+
115
+ function runtimeMetadata(payload = {}) {
116
+ const hasReady = Object.prototype.hasOwnProperty.call(payload, 'runtime_ready');
117
+ const runtimeReady = hasReady
118
+ ? payload.runtime_ready === true
119
+ : null;
120
+ const runtimeReason = runtimeReady === false
121
+ ? (String(payload.runtime_reason || 'LOCAL_EXECUTION_ENGINE_UNAVAILABLE').trim().slice(0, 96) || 'LOCAL_EXECUTION_ENGINE_UNAVAILABLE')
122
+ : null;
123
+ const executionRuntimeGeneration = String(payload.execution_runtime_generation || '').trim().slice(0, 128) || null;
124
+ return { runtimeReady, runtimeReason, executionRuntimeGeneration };
125
+ }
126
+
127
+ function effectiveRuntimeGeneration(device) {
128
+ return device.executionRuntimeGeneration || `legacy:${device.connectionEpoch}`;
129
+ }
130
+
131
+ function publicDevice(device, { stored = null, usage = null, schema = null } = {}) {
132
+ return {
133
+ deviceId: device.deviceId,
134
+ deviceName: stored?.deviceName || device.deviceId,
135
+ hostname: device.hostname || stored?.hostname || null,
136
+ platform: device.platform || stored?.platform || null,
137
+ arch: device.arch || stored?.arch || null,
138
+ pathStyle: device.pathStyle || stored?.pathStyle || null,
139
+ account: {
140
+ connected: Boolean(stored?.ownerAccountId),
141
+ account_id: stored?.ownerAccountId || null,
142
+ label: stored?.accountLabel || null
143
+ },
144
+ online: Boolean(device.online),
145
+ revoked: Boolean(stored?.revokedAt || device.revoked),
146
+ connectionEpoch: device.connectionEpoch || 0,
147
+ runtimeReady: device.runtimeReady ?? null,
148
+ runtimeReason: device.runtimeReason || null,
149
+ executionRuntimeGeneration: device.executionRuntimeGeneration || null,
150
+ agentVersion: device.agentVersion || stored?.agentVersion || 'unknown',
151
+ packageVersion: device.packageVersion || stored?.packageVersion || null,
152
+ capabilities: [...(device.capabilities || [])],
153
+ connectedAt: device.connectedAt || null,
154
+ lastSeenAt: device.lastSeenAt || null,
155
+ usage,
156
+ schema
157
+ };
158
+ }
159
+
160
+ function authFailure(ws, message = 'authentication failed') {
161
+ if (ws.readyState === WebSocket.OPEN) ws.close(4003, message.slice(0, 120));
162
+ }
163
+
164
+ function deviceBrokerError(message, code) {
165
+ const error = new Error(message);
166
+ error.code = code;
167
+ return error;
168
+ }
169
+
170
+ export function createDeviceBroker(options = {}) {
171
+ const enrollmentToken = String(options.enrollmentToken || '').trim();
172
+ const deviceStore = options.deviceStore || null;
173
+ const pairingStore = options.pairingStore || null;
174
+ const usageStore = options.usageStore || null;
175
+ const auditRecorder = options.auditRecorder || null;
176
+ const onDeviceForgotten = typeof options.onDeviceForgotten === 'function' ? options.onDeviceForgotten : null;
177
+ const innerTls = options.innerTls || null;
178
+ const durableAuth = Boolean(deviceStore);
179
+ const requireAccountOwnership = options.requireAccountOwnership !== false;
180
+ let schemaSnapshot = normalizeSchemaSnapshot(options.schemaSnapshot);
181
+ const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
182
+ const helloTimeoutMs = options.helloTimeoutMs ?? DEFAULT_HELLO_TIMEOUT_MS;
183
+ const updateTimeoutMs = options.updateTimeoutMs ?? DEFAULT_UPDATE_TIMEOUT_MS;
184
+ const devices = new Map();
185
+ const pending = new Map();
186
+ const pendingByWireRequestId = new Map();
187
+ const updateStates = new Map();
188
+ const updateTimers = new Map();
189
+ const activityStates = new Map();
190
+ const sockets = new Set();
191
+ // Internal adapters are registered by object identity, never by a wire-provided flag.
192
+ const localEndpoints = new WeakSet();
193
+ const wss = new WebSocketServer({ noServer: true, maxPayload: MAX_MESSAGE_BYTES });
194
+ let attachedServer = null;
195
+ let upgradeHandler = null;
196
+
197
+ function normalizeSchemaSnapshot(value = {}) {
198
+ const toolCount = Number.isInteger(Number(value.toolCount)) ? Number(value.toolCount) : 0;
199
+ const toolSchemaBytes = Number.isInteger(Number(value.toolSchemaBytes)) ? Number(value.toolSchemaBytes) : 0;
200
+ const toolSchemaTokenEstimate = Number.isInteger(Number(value.toolSchemaTokenEstimate))
201
+ ? Number(value.toolSchemaTokenEstimate)
202
+ : Math.ceil(toolSchemaBytes / 4);
203
+ return {
204
+ toolCount,
205
+ toolSchemaBytes,
206
+ toolSchemaTokenEstimate,
207
+ tokenEstimateMethod: String(value.tokenEstimateMethod || 'utf8_bytes_div_4_estimate').slice(0, 64),
208
+ tokenUsageKind: 'schema_estimate_not_billing'
209
+ };
210
+ }
211
+
212
+ function statusForDevice(device) {
213
+ const stored = durableAuth ? deviceStore.get(device.deviceId) : null;
214
+ const usage = usageStore ? usageStore.get(device.deviceId) : null;
215
+ const status = publicDevice(device, { stored, usage, schema: schemaSnapshot });
216
+ return { ...status, update: updateStates.get(device.deviceId) || null };
217
+ }
218
+
219
+ function clearUpdateTimer(deviceId) {
220
+ const timer = updateTimers.get(deviceId);
221
+ if (timer) clearTimeout(timer);
222
+ updateTimers.delete(deviceId);
223
+ }
224
+
225
+ function armUpdateTimer(deviceId, requestId) {
226
+ clearUpdateTimer(deviceId);
227
+ const delay = Number(updateTimeoutMs) > 0 ? Number(updateTimeoutMs) : DEFAULT_UPDATE_TIMEOUT_MS;
228
+ const timer = setTimeout(() => {
229
+ updateTimers.delete(deviceId);
230
+ const current = updateStates.get(deviceId);
231
+ if (!current || current.requestId !== requestId || !['requested', 'accepted', 'installed'].includes(current.state)) return;
232
+ updateStates.set(deviceId, {
233
+ ...current,
234
+ state: 'failed',
235
+ updatedAt: Date.now(),
236
+ error: {
237
+ code: 'DEVICE_UPDATE_TIMEOUT',
238
+ message: 'Device update stopped reporting progress before completion; retry only after the device is online.'
239
+ }
240
+ });
241
+ }, delay);
242
+ timer.unref?.();
243
+ updateTimers.set(deviceId, timer);
244
+ }
245
+
246
+ function recordDeviceStatus(device, status) {
247
+ if (!usageStore || !device?.deviceId) return;
248
+ const stored = durableAuth ? deviceStore.get(device.deviceId) : null;
249
+ usageStore.recordDeviceStatus(device.deviceId, {
250
+ accountId: stored?.ownerAccountId || null,
251
+ status,
252
+ connectionEpoch: device.connectionEpoch || 0,
253
+ agentVersion: device.agentVersion || null
254
+ });
255
+ }
256
+
257
+ function noteActivitySent(deviceId, tool) {
258
+ const previous = activityStates.get(deviceId) || {};
259
+ activityStates.set(deviceId, {
260
+ ...previous,
261
+ lastTool: String(tool || previous.lastTool || ''),
262
+ lastSentAt: Date.now()
263
+ });
264
+ }
265
+
266
+ function noteActivityFinished(deviceId, tool, outcome = 'success') {
267
+ const previous = activityStates.get(deviceId) || {};
268
+ activityStates.set(deviceId, {
269
+ ...previous,
270
+ lastTool: String(tool || previous.lastTool || ''),
271
+ lastReceivedAt: Date.now(),
272
+ lastOutcome: String(outcome || 'success')
273
+ });
274
+ }
275
+
276
+ function getActivitySnapshot({ accountId = null } = {}) {
277
+ const owner = String(accountId || '').trim();
278
+ if (durableAuth && requireAccountOwnership && !owner) return [];
279
+ const pendingByDevice = new Map();
280
+ for (const entry of pending.values()) {
281
+ const current = pendingByDevice.get(entry.deviceId) || { count: 0, tool: null, startedAt: 0 };
282
+ current.count += 1;
283
+ if (Number(entry.startedAt || 0) >= current.startedAt) {
284
+ current.startedAt = Number(entry.startedAt || 0);
285
+ current.tool = entry.tool || current.tool;
286
+ }
287
+ pendingByDevice.set(entry.deviceId, current);
288
+ }
289
+ return [...devices.values()].flatMap(device => {
290
+ const stored = durableAuth ? deviceStore.get(device.deviceId) : null;
291
+ if (durableAuth && !stored) return [];
292
+ if (owner && stored?.ownerAccountId !== owner) return [];
293
+ const activity = activityStates.get(device.deviceId) || {};
294
+ const active = pendingByDevice.get(device.deviceId) || { count: 0, tool: null };
295
+ return [{
296
+ deviceId: device.deviceId,
297
+ inFlight: active.count,
298
+ tool: active.tool || activity.lastTool || null,
299
+ lastSentAt: activity.lastSentAt || null,
300
+ lastReceivedAt: activity.lastReceivedAt || null,
301
+ lastOutcome: activity.lastOutcome || null
302
+ }];
303
+ });
304
+ }
305
+
306
+ function statusPayload(device) {
307
+ const status = statusForDevice(device);
308
+ return {
309
+ accepted: true,
310
+ account: status.account,
311
+ device: {
312
+ id: status.deviceId,
313
+ name: status.deviceName,
314
+ online: status.online,
315
+ connectionEpoch: status.connectionEpoch,
316
+ runtimeReady: status.runtimeReady,
317
+ runtimeReason: status.runtimeReason,
318
+ executionRuntimeGeneration: status.executionRuntimeGeneration,
319
+ connectedAt: status.connectedAt,
320
+ lastSeenAt: status.lastSeenAt
321
+ },
322
+ usage: status.usage,
323
+ schema: status.schema
324
+ };
325
+ }
326
+
327
+ function socketProtocolVersion(ws) {
328
+ return ws?.deviceProtocolVersion === DEVICE_PROTOCOL_VERSION_V2
329
+ ? DEVICE_PROTOCOL_VERSION_V2
330
+ : DEVICE_PROTOCOL_VERSION;
331
+ }
332
+
333
+ function sendDeviceMessage(ws, message, callback = undefined) {
334
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
335
+ callback?.(new Error('Device WebSocket is not open.'));
336
+ return false;
337
+ }
338
+ if (localEndpoints.has(ws)) {
339
+ ws.deliver(message, callback);
340
+ return true;
341
+ }
342
+ const protocolVersion = socketProtocolVersion(ws);
343
+ const payload = { ...message, protocol_version: protocolVersion };
344
+ if (protocolVersion === DEVICE_PROTOCOL_VERSION_V2) {
345
+ if (!ws.innerTlsSocket || ws.innerTlsSocket.destroyed) {
346
+ callback?.(new Error('Device inner TLS socket is not open.'));
347
+ return false;
348
+ }
349
+ const frame = encodeJsonFrame(payload, MAX_MESSAGE_BYTES);
350
+ ws.innerTlsSocket.write(frame, callback);
351
+ return true;
352
+ }
353
+ ws.send(JSON.stringify(payload), callback);
354
+ return true;
355
+ }
356
+
357
+ function sendStatusSnapshot(ws, device) {
358
+ if (!ws || ws.readyState !== WebSocket.OPEN || !device) return;
359
+ sendDeviceMessage(ws, {
360
+ type: 'status_snapshot',
361
+ device_id: device.deviceId,
362
+ connection_epoch: device.connectionEpoch,
363
+ timestamp: Date.now(),
364
+ payload: statusPayload(device)
365
+ });
366
+ }
367
+
368
+ if (durableAuth) {
369
+ for (const stored of deviceStore.list()) {
370
+ if (stored.revokedAt) {
371
+ deviceStore.revoke(stored.deviceId);
372
+ auditRecorder?.recordEvent?.({
373
+ event: 'legacy_revoked_device_purged',
374
+ callerCategory: 'migration',
375
+ deviceId: stored.deviceId,
376
+ details: { deviceName: stored.deviceName || stored.deviceId }
377
+ });
378
+ continue;
379
+ }
380
+ devices.set(stored.deviceId, {
381
+ deviceId: stored.deviceId,
382
+ socket: null,
383
+ online: false,
384
+ revoked: false,
385
+ connectionEpoch: 0,
386
+ agentVersion: stored.agentVersion || 'unknown',
387
+ packageVersion: stored.packageVersion || null,
388
+ hostname: stored.hostname || null,
389
+ platform: stored.platform || null,
390
+ arch: stored.arch || null,
391
+ pathStyle: stored.pathStyle || null,
392
+ capabilities: [],
393
+ runtimeReady: null,
394
+ runtimeReason: null,
395
+ executionRuntimeGeneration: null,
396
+ connectedAt: null,
397
+ lastSeenAt: null,
398
+ authenticatedPublicKeyPem: null,
399
+ authenticatedAuthorizationGeneration: null
400
+ });
401
+ }
402
+ }
403
+
404
+ function rejectPendingForConnection(deviceId, connectionEpoch, reason, { code = 'DEVICE_OFFLINE', outcome = 'disconnected' } = {}) {
405
+ for (const [requestId, entry] of pending) {
406
+ if (entry.deviceId !== deviceId || entry.connectionEpoch !== connectionEpoch) continue;
407
+ clearTimeout(entry.timer);
408
+ if (pending.get(requestId) !== entry) continue;
409
+ pending.delete(requestId);
410
+ if (pendingByWireRequestId.get(entry.wireRequestId) === entry) pendingByWireRequestId.delete(entry.wireRequestId);
411
+ noteActivityFinished(entry.deviceId, entry.tool, outcome);
412
+ entry.reject(deviceBrokerError(reason, code));
413
+ }
414
+ }
415
+
416
+ function registerConnection(ws, {
417
+ deviceId,
418
+ agentVersion,
419
+ packageVersion = null,
420
+ capabilities,
421
+ hostname = null,
422
+ platform = null,
423
+ arch = null,
424
+ pathStyle = null,
425
+ runtimeReady = null,
426
+ runtimeReason = null,
427
+ executionRuntimeGeneration = null,
428
+ publicKeyPem = null,
429
+ authorizationGeneration = null
430
+ }) {
431
+ const previous = devices.get(deviceId);
432
+ const connectionEpoch = (previous?.connectionEpoch || 0) + 1;
433
+ const current = {
434
+ deviceId,
435
+ socket: ws,
436
+ online: true,
437
+ revoked: false,
438
+ connectionEpoch,
439
+ agentVersion: String(agentVersion || 'unknown'),
440
+ packageVersion: String(packageVersion || '').trim().slice(0, 32) || null,
441
+ hostname: String(hostname || '').trim().slice(0, 128) || null,
442
+ platform: String(platform || '').trim().slice(0, 32) || null,
443
+ arch: String(arch || '').trim().slice(0, 32) || null,
444
+ pathStyle: ['windows', 'posix'].includes(String(pathStyle || '').trim()) ? String(pathStyle).trim() : null,
445
+ capabilities: normalizeCapabilities(capabilities),
446
+ runtimeReady: typeof runtimeReady === 'boolean' ? runtimeReady : null,
447
+ runtimeReason: runtimeReady === false ? (String(runtimeReason || 'LOCAL_EXECUTION_ENGINE_UNAVAILABLE').slice(0, 96) || 'LOCAL_EXECUTION_ENGINE_UNAVAILABLE') : null,
448
+ executionRuntimeGeneration: String(executionRuntimeGeneration || '').trim().slice(0, 128) || null,
449
+ connectedAt: Date.now(),
450
+ lastSeenAt: Date.now(),
451
+ authenticatedPublicKeyPem: durableAuth ? String(publicKeyPem || '') : null,
452
+ authenticatedAuthorizationGeneration: durableAuth ? Number(authorizationGeneration) : null
453
+ };
454
+ devices.set(deviceId, current);
455
+ const update = updateStates.get(deviceId);
456
+ if (update?.targetVersion && current.packageVersion === update.targetVersion) {
457
+ clearUpdateTimer(deviceId);
458
+ updateStates.set(deviceId, { ...update, state: 'complete', completedAt: Date.now(), error: null });
459
+ }
460
+ ws.deviceId = deviceId;
461
+ ws.connectionEpoch = connectionEpoch;
462
+ ws.authenticated = true;
463
+ if (previous?.socket && previous.socket !== ws) {
464
+ rejectPendingForConnection(
465
+ deviceId,
466
+ previous.connectionEpoch,
467
+ 'Device connection was replaced before the request result was known; the request was not replayed.'
468
+ );
469
+ if (previous.socket.readyState === WebSocket.OPEN) previous.socket.close(4001, 'replaced by newer connection');
470
+ }
471
+ return current;
472
+ }
473
+
474
+ function applyRuntimeState(device, payload = {}) {
475
+ const hasReady = Object.prototype.hasOwnProperty.call(payload, 'runtime_ready');
476
+ const hasReason = Object.prototype.hasOwnProperty.call(payload, 'runtime_reason');
477
+ const hasGeneration = Object.prototype.hasOwnProperty.call(payload, 'execution_runtime_generation');
478
+ if (!hasReady && !hasReason && !hasGeneration) return;
479
+
480
+ const beforeGeneration = effectiveRuntimeGeneration(device);
481
+ const metadata = runtimeMetadata(payload);
482
+ if (hasReady) device.runtimeReady = metadata.runtimeReady;
483
+ if (hasReady || hasReason) {
484
+ device.runtimeReason = device.runtimeReady === false
485
+ ? metadata.runtimeReason
486
+ : null;
487
+ }
488
+ if (hasGeneration) device.executionRuntimeGeneration = metadata.executionRuntimeGeneration;
489
+ const afterGeneration = effectiveRuntimeGeneration(device);
490
+
491
+ if (device.runtimeReady === false) {
492
+ rejectPendingForConnection(
493
+ device.deviceId,
494
+ device.connectionEpoch,
495
+ 'Device execution runtime became unavailable before the request result was known; the request was not replayed.',
496
+ { code: 'DEVICE_NOT_READY', outcome: 'not_ready' }
497
+ );
498
+ return;
499
+ }
500
+ if (beforeGeneration !== afterGeneration) {
501
+ rejectPendingForConnection(
502
+ device.deviceId,
503
+ device.connectionEpoch,
504
+ 'Device execution runtime changed before the request result was known; the request was not replayed.',
505
+ { code: 'DEVICE_RUNTIME_CHANGED', outcome: 'runtime_changed' }
506
+ );
507
+ }
508
+ }
509
+
510
+ function invalidateAuthorization(device, { revoked, reason, closeCode }) {
511
+ const socket = device.socket;
512
+ const connectionEpoch = device.connectionEpoch;
513
+ rejectPendingForConnection(
514
+ device.deviceId,
515
+ connectionEpoch,
516
+ `${reason} before the request result was known; the request was not replayed.`
517
+ );
518
+ if (revoked) {
519
+ clearUpdateTimer(device.deviceId);
520
+ updateStates.delete(device.deviceId);
521
+ activityStates.delete(device.deviceId);
522
+ devices.delete(device.deviceId);
523
+ try { onDeviceForgotten?.({ deviceId: device.deviceId, accountId: null }); }
524
+ catch (error) { console.error(`[device-broker] device cleanup callback failed: ${error.message}`); }
525
+ auditRecorder?.recordEvent?.({
526
+ event: 'device_forget_observed',
527
+ callerCategory: 'broker',
528
+ deviceId: device.deviceId,
529
+ details: { reason: String(reason || 'authorization removed').slice(0, 160) }
530
+ });
531
+ if (socket?.readyState === WebSocket.OPEN) socket.close(closeCode, reason.slice(0, 120));
532
+ return;
533
+ }
534
+ device.revoked = false;
535
+ device.online = false;
536
+ device.socket = null;
537
+ device.lastSeenAt = Date.now();
538
+ device.authenticatedPublicKeyPem = null;
539
+ device.authenticatedAuthorizationGeneration = null;
540
+ recordDeviceStatus(device, 'offline');
541
+ if (socket?.readyState === WebSocket.OPEN) socket.close(closeCode, reason.slice(0, 120));
542
+ }
543
+
544
+ function refreshAuthorization(device) {
545
+ if (!durableAuth || !device?.online || !device.socket) return true;
546
+ const stored = deviceStore.get(device.deviceId);
547
+ if (!stored || stored.revokedAt) {
548
+ invalidateAuthorization(device, { revoked: true, reason: 'Device was revoked', closeCode: 4004 });
549
+ return false;
550
+ }
551
+ if (!device.authenticatedPublicKeyPem || stored.publicKeyPem !== device.authenticatedPublicKeyPem || stored.authorizationGeneration !== device.authenticatedAuthorizationGeneration) {
552
+ invalidateAuthorization(device, { revoked: false, reason: 'Device authorization changed', closeCode: 4005 });
553
+ return false;
554
+ }
555
+ return true;
556
+ }
557
+
558
+ function sendAuthChallenge(ws, authState) {
559
+ const nonce = randomBytes(32).toString('base64url');
560
+ const protocolVersion = socketProtocolVersion(ws);
561
+ let exporter = null;
562
+ if (protocolVersion === DEVICE_PROTOCOL_VERSION_V2) {
563
+ exporter = ws.innerTlsSocket.exportKeyingMaterial(
564
+ 32,
565
+ DEVICE_AUTH_EXPORTER_LABEL,
566
+ buildDeviceAuthExporterContext({ deviceId: authState.deviceId, mode: authState.mode })
567
+ );
568
+ }
569
+ ws.authState = { ...authState, nonce, exporter };
570
+ sendDeviceMessage(ws, {
571
+ type: 'auth_challenge',
572
+ device_id: authState.deviceId,
573
+ timestamp: Date.now(),
574
+ payload: {
575
+ nonce,
576
+ ...(protocolVersion === DEVICE_PROTOCOL_VERSION_V2 ? { mode: authState.mode } : {})
577
+ }
578
+ });
579
+ }
580
+
581
+ function handleAuthHello(ws, message) {
582
+ const deviceId = normalizeDeviceId(message.device_id);
583
+ const v2 = socketProtocolVersion(ws) === DEVICE_PROTOCOL_VERSION_V2;
584
+ const innerCredential = v2 ? String(message.payload?.enrollment_grant || '').trim() : '';
585
+ if (message.type === 'enroll_hello') {
586
+ const pairingCredential = v2 ? innerCredential : String(ws.enrollmentCredential || '').trim();
587
+ const legacyEnrollmentAuthorized = v2
588
+ ? tokenMatches(pairingCredential, enrollmentToken)
589
+ : Boolean(ws.enrollmentAuthorized);
590
+ if (!legacyEnrollmentAuthorized && !(pairingStore && pairingCredential)) {
591
+ return authFailure(ws, 'enrollment authorization required');
592
+ }
593
+ const existing = deviceStore.get(deviceId);
594
+ if (existing) return authFailure(ws, existing.revokedAt ? 'device revoked' : 'device already enrolled');
595
+ const publicKeyPem = String(message.payload?.public_key_pem || '');
596
+ if (!publicKeyPem) return authFailure(ws, 'public key required');
597
+ sendAuthChallenge(ws, {
598
+ mode: 'enroll',
599
+ deviceId,
600
+ publicKeyPem,
601
+ agentVersion: message.payload?.agent_version,
602
+ packageVersion: message.payload?.package_version,
603
+ capabilities: message.payload?.capabilities,
604
+ ...machineMetadata(message.payload),
605
+ ...runtimeMetadata(message.payload),
606
+ legacyEnrollmentAuthorized,
607
+ enrollmentGrant: pairingCredential
608
+ });
609
+ return;
610
+ }
611
+ const stored = deviceStore.get(deviceId);
612
+ // In-process proof is not a network protocol downgrade; the persisted wire floor remains unchanged.
613
+ if (!v2 && stored?.minProtocol >= 2 && !localEndpoints.has(ws)) return authFailure(ws, 'device requires protocol v2');
614
+ if (message.type === 'pair_hello') {
615
+ const pairingCredential = v2 ? innerCredential : String(ws.enrollmentCredential || '').trim();
616
+ if (!pairingStore || !pairingCredential) return authFailure(ws, 'pairing grant required');
617
+ if (stored?.revokedAt) return authFailure(ws, 'revoked device');
618
+ const publicKeyPem = stored?.publicKeyPem || String(message.payload?.public_key_pem || '');
619
+ if (!publicKeyPem) return authFailure(ws, 'public key required for device migration');
620
+ sendAuthChallenge(ws, {
621
+ mode: 'pair',
622
+ deviceId,
623
+ publicKeyPem,
624
+ agentVersion: message.payload?.agent_version,
625
+ packageVersion: message.payload?.package_version,
626
+ capabilities: message.payload?.capabilities,
627
+ ...machineMetadata(message.payload),
628
+ ...runtimeMetadata(message.payload),
629
+ authorizationGeneration: stored?.authorizationGeneration ?? null,
630
+ wasEnrolled: Boolean(stored),
631
+ enrollmentGrant: pairingCredential
632
+ });
633
+ return;
634
+ }
635
+ if (message.type !== 'auth_hello') return authFailure(ws, 'first message must be enroll_hello, pair_hello, or auth_hello');
636
+ if (!stored || stored.revokedAt) return authFailure(ws, 'unknown or revoked device');
637
+ sendAuthChallenge(ws, {
638
+ mode: 'reconnect',
639
+ deviceId,
640
+ publicKeyPem: stored.publicKeyPem,
641
+ agentVersion: message.payload?.agent_version,
642
+ packageVersion: message.payload?.package_version,
643
+ capabilities: message.payload?.capabilities,
644
+ ...machineMetadata(message.payload),
645
+ ...runtimeMetadata(message.payload),
646
+ authorizationGeneration: stored.authorizationGeneration
647
+ });
648
+ }
649
+
650
+ function handleAuthResponse(ws, message) {
651
+ const state = ws.authState;
652
+ if (!state || message.type !== 'auth_response' || normalizeDeviceId(message.device_id) !== state.deviceId) {
653
+ return authFailure(ws, 'invalid authentication response');
654
+ }
655
+ let signature;
656
+ try { signature = Buffer.from(String(message.payload?.signature || ''), 'base64'); } catch { return authFailure(ws); }
657
+ const challenge = socketProtocolVersion(ws) === DEVICE_PROTOCOL_VERSION_V2
658
+ ? buildDeviceAuthChallengeV2({
659
+ deviceId: state.deviceId,
660
+ mode: state.mode,
661
+ nonce: state.nonce,
662
+ exporter: state.exporter,
663
+ publicKeyPem: state.publicKeyPem,
664
+ grant: state.enrollmentGrant
665
+ })
666
+ : buildDeviceAuthChallenge({ deviceId: state.deviceId, nonce: state.nonce });
667
+ const valid = signature.length > 0 && verify(null, challenge, state.publicKeyPem, signature);
668
+ if (!valid) return authFailure(ws, 'invalid signature');
669
+
670
+ let authorizedPublicKeyPem = state.publicKeyPem;
671
+ let authorizationGeneration;
672
+ if (state.mode === 'enroll') {
673
+ try {
674
+ let pairing = null;
675
+ if (!state.legacyEnrollmentAuthorized) {
676
+ pairing = pairingStore.consumeGrant({
677
+ enrollmentGrant: state.enrollmentGrant,
678
+ deviceId: state.deviceId,
679
+ publicKeyPem: state.publicKeyPem
680
+ });
681
+ }
682
+ const enrolled = deviceStore.enroll({
683
+ deviceId: state.deviceId,
684
+ publicKeyPem: state.publicKeyPem,
685
+ deviceName: pairing?.deviceName || state.deviceId,
686
+ ownerAccountId: pairing?.account?.account_id || null,
687
+ accountLabel: pairing?.account?.label || null,
688
+ hostname: state.hostname,
689
+ platform: state.platform,
690
+ arch: state.arch,
691
+ pathStyle: state.pathStyle,
692
+ agentVersion: state.agentVersion,
693
+ packageVersion: state.packageVersion
694
+ });
695
+ authorizedPublicKeyPem = enrolled.publicKeyPem;
696
+ authorizationGeneration = enrolled.authorizationGeneration;
697
+ } catch {
698
+ return authFailure(ws, 'enrollment failed');
699
+ }
700
+ } else if (state.mode === 'pair') {
701
+ try {
702
+ const pairing = pairingStore.consumeGrant({
703
+ enrollmentGrant: state.enrollmentGrant,
704
+ deviceId: state.deviceId,
705
+ publicKeyPem: state.publicKeyPem
706
+ });
707
+ const current = deviceStore.get(state.deviceId);
708
+ let updated;
709
+ if (!current) {
710
+ updated = deviceStore.enroll({
711
+ deviceId: state.deviceId,
712
+ publicKeyPem: state.publicKeyPem,
713
+ deviceName: pairing.deviceName,
714
+ ownerAccountId: pairing.account?.account_id || null,
715
+ accountLabel: pairing.account?.label || null,
716
+ hostname: state.hostname,
717
+ platform: state.platform,
718
+ arch: state.arch,
719
+ pathStyle: state.pathStyle,
720
+ agentVersion: state.agentVersion,
721
+ packageVersion: state.packageVersion
722
+ });
723
+ } else {
724
+ if (current.revokedAt || current.publicKeyPem !== state.publicKeyPem) throw new Error('device authorization changed');
725
+ if (state.wasEnrolled && current.authorizationGeneration !== state.authorizationGeneration) throw new Error('device authorization changed');
726
+ updated = deviceStore.updateMetadata({
727
+ deviceId: state.deviceId,
728
+ deviceName: pairing.deviceName,
729
+ hostname: state.hostname,
730
+ platform: state.platform,
731
+ arch: state.arch,
732
+ pathStyle: state.pathStyle,
733
+ agentVersion: state.agentVersion,
734
+ packageVersion: state.packageVersion
735
+ });
736
+ updated = deviceStore.assignOwner({
737
+ deviceId: state.deviceId,
738
+ ownerAccountId: pairing.account?.account_id || null,
739
+ accountLabel: pairing.account?.label || null
740
+ });
741
+ }
742
+ authorizedPublicKeyPem = updated.publicKeyPem;
743
+ authorizationGeneration = updated.authorizationGeneration;
744
+ } catch {
745
+ return authFailure(ws, 'account pairing failed');
746
+ }
747
+ } else {
748
+ authorizedPublicKeyPem = state.publicKeyPem;
749
+ authorizationGeneration = state.authorizationGeneration;
750
+ }
751
+
752
+ try {
753
+ const current = deviceStore.withCurrentAuthorization(
754
+ { deviceId: state.deviceId, publicKeyPem: authorizedPublicKeyPem, authorizationGeneration },
755
+ () => registerConnection(ws, { ...state, publicKeyPem: authorizedPublicKeyPem, authorizationGeneration })
756
+ );
757
+ if ((state.agentVersion !== undefined || state.packageVersion !== undefined) && typeof deviceStore.updateMetadata === 'function') {
758
+ try { deviceStore.updateMetadata({ deviceId: state.deviceId, agentVersion: state.agentVersion, packageVersion: state.packageVersion }); }
759
+ catch (error) { console.error(`[device-broker] device version persist failed: ${error.message}`); }
760
+ }
761
+ if (socketProtocolVersion(ws) === DEVICE_PROTOCOL_VERSION_V2 && typeof deviceStore.raiseProtocolFloor === 'function') {
762
+ deviceStore.raiseProtocolFloor(state.deviceId, DEVICE_PROTOCOL_VERSION_V2);
763
+ }
764
+ ws.authState = null;
765
+ if (usageStore) {
766
+ const previousUsage = usageStore.get(current.deviceId);
767
+ usageStore.recordConnection(current.deviceId, { reconnect: previousUsage.connections > 0 });
768
+ recordDeviceStatus(current, 'online');
769
+ }
770
+ sendDeviceMessage(ws, {
771
+ type: 'auth_ok',
772
+ device_id: current.deviceId,
773
+ connection_epoch: current.connectionEpoch,
774
+ timestamp: Date.now(),
775
+ payload: statusPayload(current)
776
+ });
777
+ } catch { return authFailure(ws, 'device authorization changed'); }
778
+ }
779
+
780
+ function registerLegacyHello(ws, message) {
781
+ const deviceId = normalizeDeviceId(message.device_id);
782
+ const current = registerConnection(ws, {
783
+ deviceId,
784
+ agentVersion: message.payload?.agent_version,
785
+ packageVersion: message.payload?.package_version,
786
+ capabilities: message.payload?.capabilities,
787
+ ...machineMetadata(message.payload),
788
+ ...runtimeMetadata(message.payload)
789
+ });
790
+ ws.send(JSON.stringify({
791
+ protocol_version: DEVICE_PROTOCOL_VERSION,
792
+ type: 'hello_ack',
793
+ device_id: deviceId,
794
+ connection_epoch: current.connectionEpoch,
795
+ timestamp: Date.now(),
796
+ payload: { accepted: true }
797
+ }));
798
+ }
799
+
800
+ function handleRegisteredMessage(ws, message) {
801
+ const device = devices.get(ws.deviceId);
802
+ if (!device || device.socket !== ws || device.connectionEpoch !== ws.connectionEpoch) return;
803
+ if (!refreshAuthorization(device)) return;
804
+ if (message.device_id && message.device_id !== ws.deviceId) throw new Error('device_id does not match authenticated connection.');
805
+ if (message.connection_epoch !== undefined && message.connection_epoch !== ws.connectionEpoch) return;
806
+ device.lastSeenAt = Date.now();
807
+ if (message.type === 'account_logout') {
808
+ if (!durableAuth) return;
809
+ const updated = deviceStore.assignOwner({ deviceId: device.deviceId, ownerAccountId: null, accountLabel: null });
810
+ device.authenticatedAuthorizationGeneration = updated.authorizationGeneration;
811
+ usageStore?.touch(device.deviceId);
812
+ sendStatusSnapshot(ws, device);
813
+ return;
814
+ }
815
+ if (message.type === 'heartbeat') {
816
+ applyRuntimeState(device, message.payload || {});
817
+ usageStore?.touch(device.deviceId);
818
+ sendStatusSnapshot(ws, device);
819
+ return;
820
+ }
821
+ if (message.type === 'capability_sync') {
822
+ device.capabilities = normalizeCapabilities(message.payload?.capabilities);
823
+ device.agentVersion = String(message.payload?.agent_version || device.agentVersion);
824
+ device.packageVersion = String(message.payload?.package_version || device.packageVersion || '').trim().slice(0, 32) || null;
825
+ if (durableAuth && (message.payload?.agent_version !== undefined || message.payload?.package_version !== undefined)) {
826
+ deviceStore.updateMetadata({ deviceId: device.deviceId, agentVersion: device.agentVersion, packageVersion: device.packageVersion });
827
+ }
828
+ usageStore?.touch(device.deviceId);
829
+ sendStatusSnapshot(ws, device);
830
+ return;
831
+ }
832
+ if (message.type === 'device_update_status') {
833
+ const current = updateStates.get(device.deviceId);
834
+ const requestId = String(message.request_id || '').trim();
835
+ const targetVersion = String(message.payload?.target_version || '').trim();
836
+ if (!current || current.requestId !== requestId || current.targetVersion !== targetVersion) return;
837
+ const state = String(message.payload?.state || '').trim();
838
+ if (!['accepted', 'installed', 'failed'].includes(state)) return;
839
+ updateStates.set(device.deviceId, {
840
+ ...current,
841
+ state,
842
+ updatedAt: Date.now(),
843
+ error: state === 'failed' ? {
844
+ code: String(message.payload?.code || 'DEVICE_UPDATE_FAILED').slice(0, 64),
845
+ message: String(message.payload?.message || 'Device update failed.').slice(0, 240)
846
+ } : null
847
+ });
848
+ if (state === 'failed') clearUpdateTimer(device.deviceId);
849
+ else armUpdateTimer(device.deviceId, requestId);
850
+ return;
851
+ }
852
+ if (message.type !== 'tool_result' && message.type !== 'tool_error') return;
853
+ const wireRequestId = String(message.request_id || '');
854
+ const responseBytes = Buffer.byteLength(JSON.stringify(message), 'utf8');
855
+ let usageOutcome = null;
856
+ const acceptResult = () => {
857
+ const entry = pendingByWireRequestId.get(wireRequestId);
858
+ if (!entry || entry.deviceId !== ws.deviceId || entry.connectionEpoch !== ws.connectionEpoch || pending.get(entry.requestId) !== entry) return;
859
+ clearTimeout(entry.timer);
860
+ pending.delete(entry.requestId);
861
+ if (pendingByWireRequestId.get(wireRequestId) === entry) pendingByWireRequestId.delete(wireRequestId);
862
+ noteActivityFinished(device.deviceId, entry.tool, message.type === 'tool_error' ? 'error' : 'success');
863
+ if (message.type === 'tool_error') {
864
+ const error = new Error(String(message.payload?.message || 'Remote device tool error.'));
865
+ error.code = String(message.payload?.code || 'REMOTE_DEVICE_ERROR').slice(0, 64);
866
+ usageOutcome = { ok: false, errorCode: error.code };
867
+ entry.reject(error);
868
+ } else {
869
+ usageOutcome = { ok: true };
870
+ entry.resolve(entry.includeDispatchContext
871
+ ? { result: message.payload, dispatchContext: entry.dispatchContext }
872
+ : message.payload);
873
+ }
874
+ };
875
+ try {
876
+ if (!durableAuth) acceptResult();
877
+ else {
878
+ deviceStore.withCurrentAuthorization(
879
+ { deviceId: device.deviceId, publicKeyPem: device.authenticatedPublicKeyPem, authorizationGeneration: device.authenticatedAuthorizationGeneration },
880
+ acceptResult
881
+ );
882
+ }
883
+ if (usageOutcome && usageStore) {
884
+ if (usageOutcome.ok) usageStore.recordToolSucceeded(device.deviceId, responseBytes);
885
+ else usageStore.recordToolFailed(device.deviceId, responseBytes, usageOutcome.errorCode);
886
+ sendStatusSnapshot(ws, device);
887
+ }
888
+ } catch {
889
+ invalidateAuthorization(device, { revoked: false, reason: 'Device authorization changed', closeCode: 4005 });
890
+ }
891
+ }
892
+
893
+ wss.on('connection', (ws, request) => {
894
+ sockets.add(ws);
895
+ ws.deviceProtocolVersion = ws.protocol === DEVICE_INNER_TLS_SUBPROTOCOL
896
+ ? DEVICE_PROTOCOL_VERSION_V2
897
+ : DEVICE_PROTOCOL_VERSION;
898
+ const helloTimer = setTimeout(() => {
899
+ if (!ws.authenticated && ws.readyState === WebSocket.OPEN) ws.close(4000, 'authentication required');
900
+ }, helloTimeoutMs);
901
+ helloTimer.unref?.();
902
+
903
+ const handleLogicalMessage = message => {
904
+ try {
905
+ validateMessage(message, socketProtocolVersion(ws));
906
+ if (!ws.authenticated) {
907
+ if (durableAuth) {
908
+ if (ws.authState) handleAuthResponse(ws, message);
909
+ else handleAuthHello(ws, message);
910
+ if (ws.authenticated) clearTimeout(helloTimer);
911
+ return;
912
+ }
913
+ if (message.type !== 'hello') throw new Error('First device message must be hello.');
914
+ registerLegacyHello(ws, message);
915
+ clearTimeout(helloTimer);
916
+ return;
917
+ }
918
+ handleRegisteredMessage(ws, message);
919
+ } catch (error) {
920
+ if (ws.readyState === WebSocket.OPEN) ws.close(durableAuth ? 4003 : 4002, String(error.message).slice(0, 120));
921
+ }
922
+ };
923
+
924
+ if (ws.deviceProtocolVersion === DEVICE_PROTOCOL_VERSION_V2) {
925
+ if (!innerTls) {
926
+ ws.close(1011, 'device inner TLS is unavailable');
927
+ } else {
928
+ const secureSocket = createServerInnerTls(ws, innerTls);
929
+ ws.innerTlsSocket = secureSocket;
930
+ const frameParser = createJsonFrameParser({
931
+ maxMessageBytes: MAX_MESSAGE_BYTES,
932
+ onMessage: handleLogicalMessage
933
+ });
934
+ secureSocket.on('data', chunk => {
935
+ try { frameParser.push(chunk); }
936
+ catch (error) {
937
+ if (ws.readyState === WebSocket.OPEN) ws.close(4003, String(error.message).slice(0, 120));
938
+ }
939
+ });
940
+ secureSocket.once('error', error => {
941
+ if (ws.readyState === WebSocket.OPEN) ws.close(4003, String(error.message).slice(0, 120));
942
+ });
943
+ }
944
+ } else {
945
+ ws.on('message', raw => {
946
+ try { handleLogicalMessage(parseMessage(raw)); }
947
+ catch (error) {
948
+ if (ws.readyState === WebSocket.OPEN) ws.close(durableAuth ? 4003 : 4002, String(error.message).slice(0, 120));
949
+ }
950
+ });
951
+ }
952
+
953
+ ws.on('close', () => {
954
+ sockets.delete(ws);
955
+ clearTimeout(helloTimer);
956
+ try { ws.innerTlsSocket?.destroy(); } catch {}
957
+ if (!ws.deviceId) return;
958
+ const current = devices.get(ws.deviceId);
959
+ if (!current || current.socket !== ws || current.connectionEpoch !== ws.connectionEpoch) return;
960
+ current.online = false;
961
+ current.socket = null;
962
+ current.lastSeenAt = Date.now();
963
+ recordDeviceStatus(current, 'offline');
964
+ rejectPendingForConnection(ws.deviceId, ws.connectionEpoch, 'Device disconnected before the request result was known; the request was not replayed.');
965
+ });
966
+ });
967
+
968
+ /**
969
+ * Authenticate one embedded Device and dispatch directly into the shared Core.
970
+ * The normal proof/grant handlers, pending map, owner checks and generation
971
+ * checks are shared with remote Devices. No TCP/WebSocket or child runtime exists.
972
+ */
973
+ async function attachLocalDevice({
974
+ deviceId, publicKeyPem, signChallenge, invoke, capabilities = [],
975
+ enrollmentGrant = null, ...metadata
976
+ }) {
977
+ if (!durableAuth) throw new Error('Embedded Device requires durable identity and account ownership.');
978
+ if (typeof signChallenge !== 'function' || typeof invoke !== 'function') {
979
+ throw new Error('Embedded Device requires proof signer and Core invocation adapter.');
980
+ }
981
+ const normalizedId = normalizeDeviceId(deviceId);
982
+ const suppliedKey = createPublicKey(publicKeyPem).export({ type: 'spki', format: 'pem' }).toString();
983
+ const stored = deviceStore.get(normalizedId);
984
+ if (stored && stored.publicKeyPem !== suppliedKey) {
985
+ throw deviceBrokerError('Embedded Device public key differs from enrolled identity.', 'DEVICE_IDENTITY_MISMATCH');
986
+ }
987
+ let authenticated = false;
988
+ let resolveAuth;
989
+ let rejectAuth;
990
+ const authenticatedPromise = new Promise((resolve, reject) => { resolveAuth = resolve; rejectAuth = reject; });
991
+ const endpoint = {
992
+ readyState: WebSocket.OPEN,
993
+ deviceProtocolVersion: DEVICE_PROTOCOL_VERSION,
994
+ enrollmentCredential: enrollmentGrant || '',
995
+ enrollmentAuthorized: false,
996
+ deliver(message, callback) {
997
+ if (endpoint.readyState !== WebSocket.OPEN) {
998
+ callback?.(deviceBrokerError('Embedded Device transport is closed.', 'DEVICE_OFFLINE'));
999
+ return;
1000
+ }
1001
+ if (message.type === 'auth_challenge') {
1002
+ Promise.resolve().then(() => signChallenge(message.payload.nonce)).then(signature => {
1003
+ if (endpoint.readyState !== WebSocket.OPEN) return;
1004
+ handleAuthResponse(endpoint, {
1005
+ type: 'auth_response', device_id: normalizedId,
1006
+ payload: { signature }
1007
+ });
1008
+ }).catch(error => {
1009
+ rejectAuth(error);
1010
+ endpoint.close(4003, 'identity proof failed');
1011
+ });
1012
+ } else if (message.type === 'auth_ok') {
1013
+ authenticated = true;
1014
+ resolveAuth();
1015
+ } else if (message.type === 'tool_call') {
1016
+ const envelope = { protocol_version: DEVICE_PROTOCOL_VERSION, ...message };
1017
+ if (Buffer.byteLength(JSON.stringify(envelope), 'utf8') > MAX_MESSAGE_BYTES) {
1018
+ callback?.(deviceBrokerError('Device request exceeds maximum size.', 'DEVICE_INPUT_TOO_LARGE'));
1019
+ return;
1020
+ }
1021
+ const resultBase = {
1022
+ protocol_version: DEVICE_PROTOCOL_VERSION,
1023
+ request_id: message.request_id, device_id: normalizedId,
1024
+ connection_epoch: message.connection_epoch, timestamp: Date.now()
1025
+ };
1026
+ Promise.resolve().then(() => invoke(message.payload.tool, message.payload.arguments)).then(payload => {
1027
+ const result = { ...resultBase, type: 'tool_result', payload };
1028
+ if (Buffer.byteLength(JSON.stringify(result), 'utf8') > MAX_MESSAGE_BYTES) {
1029
+ throw deviceBrokerError('Gateway device response exceeds the outbound message limit', 'DEVICE_OUTPUT_TOO_LARGE');
1030
+ }
1031
+ if (endpoint.readyState === WebSocket.OPEN) handleRegisteredMessage(endpoint, result);
1032
+ }).catch(error => {
1033
+ if (endpoint.readyState !== WebSocket.OPEN) return;
1034
+ handleRegisteredMessage(endpoint, {
1035
+ ...resultBase, type: 'tool_error',
1036
+ payload: { code: String(error?.code || 'TOOL_EXECUTION_FAILED').slice(0, 64), message: String(error?.message || error).slice(0, 4096) }
1037
+ });
1038
+ });
1039
+ } else if (message.type === 'device_update') {
1040
+ // Updating this host must restart the whole server via its service owner.
1041
+ // Never spawn a second Device or kill the control plane from a tool callback.
1042
+ const update = updateStates.get(normalizedId);
1043
+ if (update) {
1044
+ clearUpdateTimer(normalizedId);
1045
+ updateStates.set(normalizedId, { ...update, state: 'failed', updatedAt: Date.now(),
1046
+ error: { code: 'SERVER_UPDATE_REQUIRES_RESTART', message: 'Update the server package through its service owner.' } });
1047
+ }
1048
+ }
1049
+ callback?.();
1050
+ },
1051
+ close(_code, reason = 'embedded Device disconnected') {
1052
+ if (endpoint.readyState === WebSocket.CLOSED) return;
1053
+ endpoint.readyState = WebSocket.CLOSED;
1054
+ sockets.delete(endpoint);
1055
+ clearTimeout(authTimer);
1056
+ if (!authenticated) rejectAuth(deviceBrokerError(reason, 'DEVICE_AUTH_FAILED'));
1057
+ const current = devices.get(normalizedId);
1058
+ if (!current || current.socket !== endpoint || current.connectionEpoch !== endpoint.connectionEpoch) return;
1059
+ current.online = false;
1060
+ current.socket = null;
1061
+ current.lastSeenAt = Date.now();
1062
+ recordDeviceStatus(current, 'offline');
1063
+ rejectPendingForConnection(normalizedId, endpoint.connectionEpoch,
1064
+ 'Device disconnected before the request result was known; the request was not replayed.');
1065
+ },
1066
+ terminate() { endpoint.close(); }
1067
+ };
1068
+ localEndpoints.add(endpoint);
1069
+ sockets.add(endpoint);
1070
+ const authTimer = setTimeout(() => endpoint.close(4003, 'embedded identity proof timed out'), helloTimeoutMs);
1071
+ try {
1072
+ handleAuthHello(endpoint, {
1073
+ type: enrollmentGrant ? 'pair_hello' : 'auth_hello',
1074
+ device_id: normalizedId,
1075
+ payload: {
1076
+ public_key_pem: suppliedKey, capabilities,
1077
+ agent_version: metadata.agentVersion, package_version: metadata.packageVersion,
1078
+ hostname: metadata.hostname, platform: metadata.platform, arch: metadata.arch,
1079
+ path_style: metadata.pathStyle, runtime_ready: metadata.runtimeReady !== false,
1080
+ runtime_reason: metadata.runtimeReason,
1081
+ execution_runtime_generation: metadata.executionRuntimeGeneration
1082
+ }
1083
+ });
1084
+ await authenticatedPromise;
1085
+ clearTimeout(authTimer);
1086
+ return {
1087
+ deviceId: normalizedId,
1088
+ connectionEpoch: endpoint.connectionEpoch,
1089
+ isOpen: () => endpoint.readyState === WebSocket.OPEN,
1090
+ updateCapabilities(next) {
1091
+ if (endpoint.readyState !== WebSocket.OPEN) return;
1092
+ handleRegisteredMessage(endpoint, {
1093
+ type: 'capability_sync', device_id: normalizedId,
1094
+ connection_epoch: endpoint.connectionEpoch, payload: { capabilities: next }
1095
+ });
1096
+ },
1097
+ close: () => endpoint.close()
1098
+ };
1099
+ } catch (error) {
1100
+ endpoint.close();
1101
+ throw error;
1102
+ }
1103
+ }
1104
+
1105
+ function attach(server, endpointPath = '/device') {
1106
+ if (attachedServer) throw new Error('Device broker is already attached.');
1107
+ attachedServer = server;
1108
+ upgradeHandler = (request, socket, head) => {
1109
+ let pathname;
1110
+ try { pathname = new URL(request.url || '/', 'http://localhost').pathname; }
1111
+ catch { return rejectUpgrade(socket, 400, 'Bad Request'); }
1112
+ if (pathname !== endpointPath) return;
1113
+ const requestedProtocols = String(request.headers['sec-websocket-protocol'] || '')
1114
+ .split(',')
1115
+ .map(value => value.trim())
1116
+ .filter(Boolean);
1117
+ const requestsV2 = requestedProtocols.includes(DEVICE_INNER_TLS_SUBPROTOCOL);
1118
+ if (requestsV2 && !innerTls) return rejectUpgrade(socket, 503, 'Device Inner TLS Unavailable');
1119
+ if (requestedProtocols.length > 0 && !requestsV2) return rejectUpgrade(socket, 400, 'Unsupported Device Protocol');
1120
+ if (!durableAuth) {
1121
+ if (!enrollmentToken) return rejectUpgrade(socket, 503, 'Device Enrollment Disabled');
1122
+ if (!tokenMatches(bearerToken(request), enrollmentToken)) return rejectUpgrade(socket, 401, 'Unauthorized');
1123
+ }
1124
+ wss.handleUpgrade(request, socket, head, ws => {
1125
+ const v2 = ws.protocol === DEVICE_INNER_TLS_SUBPROTOCOL;
1126
+ const credential = v2 ? '' : bearerToken(request);
1127
+ ws.enrollmentAuthorized = !v2 && tokenMatches(credential, enrollmentToken);
1128
+ ws.enrollmentCredential = ws.enrollmentAuthorized ? '' : credential;
1129
+ wss.emit('connection', ws, request);
1130
+ });
1131
+ };
1132
+ server.on('upgrade', upgradeHandler);
1133
+ }
1134
+
1135
+ function listDevices({ accountId = null } = {}) {
1136
+ for (const device of devices.values()) {
1137
+ if (device.online) refreshAuthorization(device);
1138
+ }
1139
+ const owner = String(accountId || '').trim();
1140
+ if (durableAuth && requireAccountOwnership && !owner) return [];
1141
+ return [...devices.values()]
1142
+ .map(statusForDevice)
1143
+ .filter(device => !owner || device.account.account_id === owner)
1144
+ .sort((a, b) => a.deviceId.localeCompare(b.deviceId));
1145
+ }
1146
+
1147
+ function requireOwnedDevice(accountId, deviceId) {
1148
+ if (!durableAuth) throw new Error('Durable device store is required for account-owned device operations.');
1149
+ const owner = String(accountId || '').trim();
1150
+ const normalized = normalizeDeviceId(deviceId);
1151
+ if (!owner) throw deviceBrokerError('ACCOUNT_ID_REQUIRED: account_id is required for device management.', 'ACCOUNT_ID_REQUIRED');
1152
+ const stored = deviceStore.get(normalized);
1153
+ if (!stored || !stored.ownerAccountId || stored.ownerAccountId !== owner) {
1154
+ throw deviceBrokerError('DEVICE_ACCESS_DENIED: device is not owned by the caller account.', 'DEVICE_ACCESS_DENIED');
1155
+ }
1156
+ return stored;
1157
+ }
1158
+
1159
+ function renameOwnedDevice({ accountId, deviceId, deviceName }) {
1160
+ const stored = requireOwnedDevice(accountId, deviceId);
1161
+ const name = String(deviceName || '').trim();
1162
+ if (!name || name.length > 128) throw new Error('device_name must be between 1 and 128 characters.');
1163
+ deviceStore.updateMetadata({ deviceId: stored.deviceId, deviceName: name });
1164
+ const current = devices.get(stored.deviceId) || { deviceId: stored.deviceId, connectionEpoch: 0, capabilities: [] };
1165
+ if (current.online) sendStatusSnapshot(current.socket, current);
1166
+ return statusForDevice(current);
1167
+ }
1168
+
1169
+ function revokeDevice(deviceId, { accountId = null, callerCategory = 'system' } = {}) {
1170
+ if (!durableAuth) throw new Error('Durable device store is required for revocation.');
1171
+ const normalized = normalizeDeviceId(deviceId);
1172
+ const stored = deviceStore.get(normalized);
1173
+ if (!stored) throw new Error(`Unknown device ${normalized}.`);
1174
+ const current = devices.get(normalized) || { deviceId: normalized, connectionEpoch: 0, capabilities: [] };
1175
+
1176
+ deviceStore.revoke(normalized);
1177
+ if (current.socket) {
1178
+ rejectPendingForConnection(
1179
+ normalized,
1180
+ current.connectionEpoch,
1181
+ 'Device was forgotten before the request result was known; the request was not replayed.'
1182
+ );
1183
+ }
1184
+ clearUpdateTimer(normalized);
1185
+ updateStates.delete(normalized);
1186
+ activityStates.delete(normalized);
1187
+ devices.delete(normalized);
1188
+ try {
1189
+ onDeviceForgotten?.({ deviceId: normalized, accountId: stored.ownerAccountId || null });
1190
+ } catch (error) {
1191
+ console.error(`[device-broker] device cleanup callback failed: ${error.message}`);
1192
+ }
1193
+ if (current.socket?.readyState === WebSocket.OPEN) current.socket.close(4004, 'device forgotten');
1194
+ auditRecorder?.recordEvent?.({
1195
+ event: 'device_forgotten',
1196
+ callerId: accountId || stored.ownerAccountId || '',
1197
+ callerCategory,
1198
+ deviceId: normalized,
1199
+ details: {
1200
+ deviceName: stored.deviceName || normalized,
1201
+ hostname: stored.hostname || null,
1202
+ packageVersion: stored.packageVersion || null
1203
+ }
1204
+ });
1205
+ return { deviceId: normalized, deviceName: stored.deviceName || normalized, forgotten: true };
1206
+ }
1207
+
1208
+ function revokeOwnedDevice({ accountId, deviceId }) {
1209
+ const stored = requireOwnedDevice(accountId, deviceId);
1210
+ return revokeDevice(stored.deviceId, { accountId, callerCategory: 'dashboard' });
1211
+ }
1212
+
1213
+ function requestDeviceUpdate({ accountId, deviceId, targetVersion }) {
1214
+ const stored = requireOwnedDevice(accountId, deviceId);
1215
+ const device = devices.get(stored.deviceId);
1216
+ if (device?.online && !refreshAuthorization(device)) {
1217
+ throw deviceBrokerError(`Device ${stored.deviceId} authorization changed or was revoked; it is offline.`, 'DEVICE_OFFLINE');
1218
+ }
1219
+ if (!device?.online || device.revoked || !device.socket || device.socket.readyState !== WebSocket.OPEN) {
1220
+ throw deviceBrokerError(`Device ${stored.deviceId} is offline.`, 'DEVICE_OFFLINE');
1221
+ }
1222
+ const target = normalizeStableVersion(targetVersion);
1223
+ let current;
1224
+ try { current = normalizeStableVersion(device.packageVersion || stored.packageVersion); }
1225
+ catch { throw deviceBrokerError('Device package version is unavailable; bootstrap MCP Device before dashboard updates.', 'DEVICE_UPDATE_BOOTSTRAP_REQUIRED'); }
1226
+ const platform = device.platform || stored.platform || null;
1227
+ const minimumVersion = minimumSelfUpdateVersion(platform);
1228
+ if (!supportsDeviceSelfUpdate(current, platform)) {
1229
+ throw deviceBrokerError(`Device must be bootstrapped to MCP Device ${minimumVersion} before dashboard self-update is available.`, 'DEVICE_UPDATE_BOOTSTRAP_REQUIRED');
1230
+ }
1231
+ if (compareStableVersions(target, current) <= 0) {
1232
+ throw deviceBrokerError(`Device is already on ${current} or newer.`, 'DEVICE_UPDATE_NOT_NEEDED');
1233
+ }
1234
+ const existing = updateStates.get(device.deviceId);
1235
+ if (existing && ['requested', 'accepted', 'installed'].includes(existing.state)) {
1236
+ if (existing.targetVersion === target) return existing;
1237
+ throw deviceBrokerError('Another device update is already in progress.', 'DEVICE_UPDATE_IN_PROGRESS');
1238
+ }
1239
+ const requestId = randomUUID();
1240
+ const update = {
1241
+ requestId,
1242
+ targetVersion: target,
1243
+ state: 'requested',
1244
+ requestedAt: Date.now(),
1245
+ updatedAt: Date.now(),
1246
+ error: null
1247
+ };
1248
+ const wireMessage = {
1249
+ type: 'device_update',
1250
+ request_id: requestId,
1251
+ device_id: device.deviceId,
1252
+ connection_epoch: device.connectionEpoch,
1253
+ timestamp: Date.now(),
1254
+ payload: { target_version: target }
1255
+ };
1256
+ const dispatch = () => {
1257
+ updateStates.set(device.deviceId, update);
1258
+ const sent = sendDeviceMessage(device.socket, wireMessage, error => {
1259
+ if (!error) return;
1260
+ const currentUpdate = updateStates.get(device.deviceId);
1261
+ if (currentUpdate?.requestId !== requestId) return;
1262
+ clearUpdateTimer(device.deviceId);
1263
+ updateStates.set(device.deviceId, {
1264
+ ...currentUpdate,
1265
+ state: 'failed',
1266
+ updatedAt: Date.now(),
1267
+ error: { code: 'DEVICE_UPDATE_SEND_ERROR', message: String(error.message || error).slice(0, 240) }
1268
+ });
1269
+ });
1270
+ if (!sent) {
1271
+ clearUpdateTimer(device.deviceId);
1272
+ updateStates.set(device.deviceId, {
1273
+ ...update,
1274
+ state: 'failed',
1275
+ updatedAt: Date.now(),
1276
+ error: { code: 'DEVICE_UPDATE_SEND_ERROR', message: 'Device update could not be sent.' }
1277
+ });
1278
+ throw deviceBrokerError('Device update could not be sent.', 'DEVICE_UPDATE_SEND_ERROR');
1279
+ }
1280
+ armUpdateTimer(device.deviceId, requestId);
1281
+ };
1282
+ deviceStore.withCurrentAuthorization(
1283
+ {
1284
+ deviceId: device.deviceId,
1285
+ publicKeyPem: device.authenticatedPublicKeyPem,
1286
+ authorizationGeneration: device.authenticatedAuthorizationGeneration
1287
+ },
1288
+ dispatch
1289
+ );
1290
+ return updateStates.get(device.deviceId);
1291
+ }
1292
+
1293
+ async function callDevice({
1294
+ requestId: requestedRequestId,
1295
+ accountId = null,
1296
+ deviceId,
1297
+ tool,
1298
+ arguments: args = {},
1299
+ timeoutMs = requestTimeoutMs,
1300
+ expectedConnectionEpoch,
1301
+ expectedExecutionRuntimeGeneration,
1302
+ includeDispatchContext = false
1303
+ }) {
1304
+ const device = devices.get(String(deviceId || ''));
1305
+ if (device?.online && !refreshAuthorization(device)) {
1306
+ throw deviceBrokerError(`Device ${deviceId} authorization changed or was revoked; it is offline.`, 'DEVICE_OFFLINE');
1307
+ }
1308
+ if (!device?.online || device.revoked || !device.socket || device.socket.readyState !== WebSocket.OPEN) {
1309
+ throw deviceBrokerError(`Device ${deviceId} is offline or unknown.`, 'DEVICE_OFFLINE');
1310
+ }
1311
+ const currentRuntimeGeneration = effectiveRuntimeGeneration(device);
1312
+ if (expectedConnectionEpoch !== undefined && String(expectedConnectionEpoch) !== String(device.connectionEpoch)) {
1313
+ throw deviceBrokerError('Process session is stale because the device connection changed.', 'PROCESS_SESSION_STALE');
1314
+ }
1315
+ if (expectedExecutionRuntimeGeneration !== undefined &&
1316
+ String(expectedExecutionRuntimeGeneration) !== currentRuntimeGeneration) {
1317
+ throw deviceBrokerError('Process session is stale because the execution runtime changed.', 'PROCESS_SESSION_STALE');
1318
+ }
1319
+ if (device.runtimeReady === false) {
1320
+ throw deviceBrokerError(
1321
+ `Device ${deviceId} execution runtime is not ready${device.runtimeReason ? `: ${device.runtimeReason}` : '.'}`,
1322
+ 'DEVICE_NOT_READY'
1323
+ );
1324
+ }
1325
+ if (durableAuth && requireAccountOwnership) {
1326
+ const owner = String(accountId || '').trim();
1327
+ const stored = deviceStore.get(device.deviceId);
1328
+ if (!owner) throw new Error('ACCOUNT_ID_REQUIRED: account_id is required for remote device dispatch.');
1329
+ if (!stored?.ownerAccountId || stored.ownerAccountId !== owner) throw new Error('DEVICE_ACCESS_DENIED: device is not owned by the caller account.');
1330
+ }
1331
+ if (!device.capabilities.includes(tool)) throw new Error(`Device ${deviceId} does not advertise capability ${tool}.`);
1332
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000) throw new Error('Device request timeout must be an integer between 1 and 30000 ms.');
1333
+ const requestId = requestedRequestId === undefined ? randomUUID() : String(requestedRequestId).trim();
1334
+ if (!requestId || requestId.length > 128) throw new Error('Device request_id must be between 1 and 128 characters.');
1335
+ if (pending.has(requestId)) throw new Error(`Device request ${requestId} is already pending.`);
1336
+ const wireRequestId = randomUUID();
1337
+ const wireMessage = {
1338
+ type: 'tool_call',
1339
+ request_id: wireRequestId,
1340
+ device_id: device.deviceId,
1341
+ connection_epoch: device.connectionEpoch,
1342
+ timestamp: Date.now(),
1343
+ payload: { tool, arguments: args }
1344
+ };
1345
+ const wireVersion = socketProtocolVersion(device.socket);
1346
+ const requestBytes = Buffer.byteLength(JSON.stringify({ ...wireMessage, protocol_version: wireVersion }), 'utf8');
1347
+ return await new Promise((resolve, reject) => {
1348
+ let entry = null;
1349
+ const dispatch = () => {
1350
+ const current = devices.get(device.deviceId);
1351
+ if (current !== device || !device.online || device.revoked ||
1352
+ !device.socket || device.socket.readyState !== WebSocket.OPEN) {
1353
+ if (expectedConnectionEpoch !== undefined || expectedExecutionRuntimeGeneration !== undefined) {
1354
+ throw deviceBrokerError('Process session is stale because the device connection changed.', 'PROCESS_SESSION_STALE');
1355
+ }
1356
+ throw deviceBrokerError(`Device ${deviceId} connection changed before dispatch.`, 'DEVICE_OFFLINE');
1357
+ }
1358
+ const dispatchRuntimeGeneration = effectiveRuntimeGeneration(device);
1359
+ if (expectedConnectionEpoch !== undefined && String(expectedConnectionEpoch) !== String(device.connectionEpoch)) {
1360
+ throw deviceBrokerError('Process session is stale because the device connection changed.', 'PROCESS_SESSION_STALE');
1361
+ }
1362
+ if (expectedExecutionRuntimeGeneration !== undefined &&
1363
+ String(expectedExecutionRuntimeGeneration) !== dispatchRuntimeGeneration) {
1364
+ throw deviceBrokerError('Process session is stale because the execution runtime changed.', 'PROCESS_SESSION_STALE');
1365
+ }
1366
+ if (device.runtimeReady === false) {
1367
+ throw deviceBrokerError(
1368
+ `Device ${deviceId} execution runtime is not ready${device.runtimeReason ? `: ${device.runtimeReason}` : '.'}`,
1369
+ 'DEVICE_NOT_READY'
1370
+ );
1371
+ }
1372
+ const dispatchContext = {
1373
+ deviceId: device.deviceId,
1374
+ connectionEpoch: device.connectionEpoch,
1375
+ executionRuntimeGeneration: dispatchRuntimeGeneration
1376
+ };
1377
+ entry = {
1378
+ requestId, wireRequestId, resolve, reject, timer: null,
1379
+ deviceId: device.deviceId, connectionEpoch: device.connectionEpoch,
1380
+ executionRuntimeGeneration: dispatchRuntimeGeneration,
1381
+ includeDispatchContext: includeDispatchContext === true,
1382
+ dispatchContext,
1383
+ tool, startedAt: Date.now()
1384
+ };
1385
+ const timer = setTimeout(() => {
1386
+ if (pending.get(requestId) !== entry) return;
1387
+ pending.delete(requestId);
1388
+ if (pendingByWireRequestId.get(wireRequestId) === entry) pendingByWireRequestId.delete(wireRequestId);
1389
+ usageStore?.recordToolFailed(device.deviceId, 0, 'DEVICE_REQUEST_TIMEOUT');
1390
+ noteActivityFinished(device.deviceId, tool, 'timeout');
1391
+ entry.reject(deviceBrokerError(`Device request ${requestId} timed out; it was not replayed.`, 'DEVICE_REQUEST_TIMEOUT'));
1392
+ }, timeoutMs);
1393
+ entry.timer = timer;
1394
+ timer.unref?.();
1395
+ pending.set(requestId, entry);
1396
+ pendingByWireRequestId.set(wireRequestId, entry);
1397
+ noteActivitySent(device.deviceId, tool);
1398
+ sendDeviceMessage(device.socket, wireMessage, error => {
1399
+ if (!error || pending.get(requestId) !== entry) return;
1400
+ clearTimeout(entry.timer);
1401
+ pending.delete(requestId);
1402
+ if (pendingByWireRequestId.get(wireRequestId) === entry) pendingByWireRequestId.delete(wireRequestId);
1403
+ usageStore?.recordToolFailed(device.deviceId, 0, 'DEVICE_SEND_ERROR');
1404
+ noteActivityFinished(device.deviceId, tool, 'send_error');
1405
+ entry.reject(error);
1406
+ });
1407
+ };
1408
+ try {
1409
+ if (durableAuth) {
1410
+ deviceStore.withCurrentAuthorization(
1411
+ {
1412
+ deviceId: device.deviceId,
1413
+ publicKeyPem: device.authenticatedPublicKeyPem,
1414
+ authorizationGeneration: device.authenticatedAuthorizationGeneration
1415
+ },
1416
+ dispatch
1417
+ );
1418
+ } else dispatch();
1419
+ usageStore?.recordToolStarted(device.deviceId, requestBytes);
1420
+ } catch (error) {
1421
+ if (entry && pending.get(requestId) === entry) {
1422
+ clearTimeout(entry.timer);
1423
+ pending.delete(requestId);
1424
+ if (pendingByWireRequestId.get(wireRequestId) === entry) pendingByWireRequestId.delete(wireRequestId);
1425
+ }
1426
+ if (durableAuth && /authorization changed|revoked|stale/i.test(String(error?.message || ''))) {
1427
+ invalidateAuthorization(device, { revoked: false, reason: 'Device authorization changed', closeCode: 4005 });
1428
+ }
1429
+ reject(error);
1430
+ }
1431
+ });
1432
+ }
1433
+
1434
+ async function shutdown() {
1435
+ if (attachedServer && upgradeHandler) attachedServer.off('upgrade', upgradeHandler);
1436
+ for (const entry of pending.values()) {
1437
+ clearTimeout(entry.timer);
1438
+ if (pending.get(entry.requestId) === entry) pending.delete(entry.requestId);
1439
+ if (pendingByWireRequestId.get(entry.wireRequestId) === entry) pendingByWireRequestId.delete(entry.wireRequestId);
1440
+ entry.reject(new Error('Device broker is shutting down.'));
1441
+ }
1442
+ pending.clear();
1443
+ pendingByWireRequestId.clear();
1444
+ for (const deviceId of updateTimers.keys()) clearUpdateTimer(deviceId);
1445
+ for (const ws of sockets) { try { ws.terminate(); } catch {} }
1446
+ sockets.clear();
1447
+ await new Promise(resolve => wss.close(() => resolve()));
1448
+ attachedServer = null;
1449
+ upgradeHandler = null;
1450
+ }
1451
+
1452
+ function setSchemaSnapshot(value) {
1453
+ schemaSnapshot = normalizeSchemaSnapshot(value);
1454
+ for (const device of devices.values()) {
1455
+ if (device.online) sendStatusSnapshot(device.socket, device);
1456
+ }
1457
+ return schemaSnapshot;
1458
+ }
1459
+
1460
+ return { attach, attachLocalDevice, listDevices, getActivitySnapshot, renameOwnedDevice, revokeDevice, revokeOwnedDevice, requestDeviceUpdate, callDevice, setSchemaSnapshot, shutdown };
1461
+ }