wirestudio 0.17.2__tar.gz → 0.17.3__tar.gz

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 (273) hide show
  1. {wirestudio-0.17.2/wirestudio.egg-info → wirestudio-0.17.3}/PKG-INFO +1 -1
  2. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_fleet.py +134 -0
  3. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_mcp_auth.py +42 -1
  4. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/__init__.py +1 -1
  5. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/api/app.py +77 -3
  6. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/api/schemas.py +9 -1
  7. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/fleet/client.py +44 -0
  8. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/mcp/__init__.py +6 -0
  9. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/mcp/auth.py +65 -14
  10. {wirestudio-0.17.2 → wirestudio-0.17.3/wirestudio.egg-info}/PKG-INFO +1 -1
  11. {wirestudio-0.17.2 → wirestudio-0.17.3}/LICENSE +0 -0
  12. {wirestudio-0.17.2 → wirestudio-0.17.3}/README.md +0 -0
  13. {wirestudio-0.17.2 → wirestudio-0.17.3}/pyproject.toml +0 -0
  14. {wirestudio-0.17.2 → wirestudio-0.17.3}/setup.cfg +0 -0
  15. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_active_design.py +0 -0
  16. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_agent.py +0 -0
  17. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_agent_stream.py +0 -0
  18. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_api.py +0 -0
  19. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_ascii_gen.py +0 -0
  20. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_build_backend.py +0 -0
  21. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_chirpstack.py +0 -0
  22. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_codec.py +0 -0
  23. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_compatibility.py +0 -0
  24. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_compile.py +0 -0
  25. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_coverage_baseline.py +0 -0
  26. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_design_events.py +0 -0
  27. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_designs.py +0 -0
  28. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_designs_seed.py +0 -0
  29. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_enclosure.py +0 -0
  30. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_enclosure_gate.py +0 -0
  31. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_enclosure_search.py +0 -0
  32. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_firmware_gen.py +0 -0
  33. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_footprints.py +0 -0
  34. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_intent.py +0 -0
  35. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_inventory.py +0 -0
  36. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_jlcpcb.py +0 -0
  37. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_kicad.py +0 -0
  38. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_kicad_fab.py +0 -0
  39. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_kicad_import.py +0 -0
  40. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_kicad_pcb.py +0 -0
  41. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_kicad_render.py +0 -0
  42. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_library_rtttl.py +0 -0
  43. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_lorawan.py +0 -0
  44. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_lorawan_api.py +0 -0
  45. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_mcp_resources.py +0 -0
  46. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_mcp_server.py +0 -0
  47. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_modules.py +0 -0
  48. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_pin_solver.py +0 -0
  49. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_recommend.py +0 -0
  50. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_schema.py +0 -0
  51. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_schematics_gate.py +0 -0
  52. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_security_cors.py +0 -0
  53. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_seed_onboard.py +0 -0
  54. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_serve.py +0 -0
  55. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_targets.py +0 -0
  56. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_tools_add_bus.py +0 -0
  57. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_tools_solve_pins.py +0 -0
  58. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_validate.py +0 -0
  59. {wirestudio-0.17.2 → wirestudio-0.17.3}/tests/test_yaml_gen.py +0 -0
  60. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/agent/__init__.py +0 -0
  61. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/agent/agent.py +0 -0
  62. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/agent/session.py +0 -0
  63. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/agent/tools.py +0 -0
  64. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/api/__init__.py +0 -0
  65. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/api/__main__.py +0 -0
  66. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/api/serve.py +0 -0
  67. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/csp/__init__.py +0 -0
  68. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/csp/compatibility.py +0 -0
  69. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/csp/pin_solver.py +0 -0
  70. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/designs/__init__.py +0 -0
  71. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/designs/active.py +0 -0
  72. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/designs/events.py +0 -0
  73. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/designs/seed.py +0 -0
  74. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/designs/store.py +0 -0
  75. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/enclosure/__init__.py +0 -0
  76. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/enclosure/openscad.py +0 -0
  77. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/enclosure/search.py +0 -0
  78. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/__init__.py +0 -0
  79. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/analog-node.json +0 -0
  80. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/atom-echo.json +0 -0
  81. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/atom-lite.json +0 -0
  82. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/atom-matrix.json +0 -0
  83. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/atoms3-lcd.json +0 -0
  84. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/atoms3-lite.json +0 -0
  85. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/atoms3-onboard.json +0 -0
  86. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/atomu-onboard.json +0 -0
  87. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/atomu.json +0 -0
  88. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/attic-logger.json +0 -0
  89. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/awning-control.json +0 -0
  90. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/bl0906-mainmeter.json +0 -0
  91. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/blind-stepper.json +0 -0
  92. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/bluemotion.json +0 -0
  93. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/bluesonoff.json +0 -0
  94. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/button-toggles-light.json +0 -0
  95. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/c3-supermini.json +0 -0
  96. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/c6-supermini.json +0 -0
  97. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/desk-climate.json +0 -0
  98. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/desk-matrix.json +0 -0
  99. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/distance-sensor.json +0 -0
  100. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/encoder-drives-stepper.json +0 -0
  101. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/esp32-audio.json +0 -0
  102. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/esp32cam-wrover.json +0 -0
  103. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/esp32cam.json +0 -0
  104. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/flow-meter.json +0 -0
  105. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/garage-motion.json +0 -0
  106. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/grill-probe.json +0 -0
  107. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/guarded-button-light.json +0 -0
  108. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/keypad.json +0 -0
  109. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/kitchen-scale.json +0 -0
  110. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/led-display.json +0 -0
  111. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/lorawan-battery-uplink.json +0 -0
  112. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/motion-turns-on-light.json +0 -0
  113. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/multi-temp.json +0 -0
  114. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/nextion-thermostat.json +0 -0
  115. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/oled-knob.json +0 -0
  116. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/oled.json +0 -0
  117. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/parking-distance.json +0 -0
  118. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/presence-rf.json +0 -0
  119. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/rc522.json +0 -0
  120. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/room-climate.json +0 -0
  121. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/rs485-energy.json +0 -0
  122. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/s3-devkitc.json +0 -0
  123. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/s3-supermini.json +0 -0
  124. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/securitypanel.json +0 -0
  125. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/smart-plug-v1.json +0 -0
  126. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/smart-plug.json +0 -0
  127. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/subghz-rfid.json +0 -0
  128. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/t-beam.json +0 -0
  129. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/temp-above-turns-on-fan.json +0 -0
  130. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/temp-turns-on-fan.json +0 -0
  131. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/tft-touch.json +0 -0
  132. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/ttgo-lora32.json +0 -0
  133. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/tuya-smart-plug.json +0 -0
  134. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/wasserpir.json +0 -0
  135. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/weather-station.json +0 -0
  136. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/examples/wemosgps.json +0 -0
  137. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/fleet/__init__.py +0 -0
  138. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/generate/__init__.py +0 -0
  139. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/generate/__main__.py +0 -0
  140. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/generate/ascii_gen.py +0 -0
  141. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/generate/yaml_gen.py +0 -0
  142. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/intent.py +0 -0
  143. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/inventory/__init__.py +0 -0
  144. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/inventory/check.py +0 -0
  145. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/inventory/csv_io.py +0 -0
  146. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/inventory/store.py +0 -0
  147. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/jlcpcb/__init__.py +0 -0
  148. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/jlcpcb/__main__.py +0 -0
  149. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/jlcpcb/check.py +0 -0
  150. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/jlcpcb/client.py +0 -0
  151. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/kicad/__init__.py +0 -0
  152. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/kicad/fab.py +0 -0
  153. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/kicad/generator.py +0 -0
  154. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/kicad/import.py +0 -0
  155. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/kicad/importer.py +0 -0
  156. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/kicad/netlist.py +0 -0
  157. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/kicad/pcb.py +0 -0
  158. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/kicad/render.py +0 -0
  159. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/kicad/symbol_parser.py +0 -0
  160. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/__init__.py +0 -0
  161. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/esp01_1m.yaml +0 -0
  162. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/esp32-c3-devkitm-1.yaml +0 -0
  163. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/esp32-c3-supermini.yaml +0 -0
  164. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/esp32-c6-supermini.yaml +0 -0
  165. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/esp32-devkitc-v4.yaml +0 -0
  166. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/esp32-s3-devkitc-1.yaml +0 -0
  167. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/esp32-s3-supermini.yaml +0 -0
  168. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/esp32-wrover-cam.yaml +0 -0
  169. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/esp32cam-ai-thinker.yaml +0 -0
  170. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/esp8285-1m.yaml +0 -0
  171. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/heltec-wifi-lora32-v2.yaml +0 -0
  172. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/heltec-wifi-lora32-v3.yaml +0 -0
  173. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/m5stack-atom-echo.yaml +0 -0
  174. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/m5stack-atom-matrix.yaml +0 -0
  175. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/m5stack-atom.yaml +0 -0
  176. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/m5stack-atoms3-lite.yaml +0 -0
  177. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/m5stack-atoms3.yaml +0 -0
  178. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/m5stack-atomu.yaml +0 -0
  179. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/nodemcu-32s.yaml +0 -0
  180. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/nodemcu-v2.yaml +0 -0
  181. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/ttgo-lora32-v1.yaml +0 -0
  182. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/ttgo-lora32-v2.yaml +0 -0
  183. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/ttgo-t-beam.yaml +0 -0
  184. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/boards/wemos-d1-mini.yaml +0 -0
  185. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/adc.yaml +0 -0
  186. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/ads1115.yaml +0 -0
  187. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/ads1115_channel.yaml +0 -0
  188. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/aht10.yaml +0 -0
  189. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/apa102.yaml +0 -0
  190. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/axp192.yaml +0 -0
  191. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/bh1750.yaml +0 -0
  192. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/bl0906.yaml +0 -0
  193. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/bme280.yaml +0 -0
  194. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/bmp180.yaml +0 -0
  195. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/bmp280.yaml +0 -0
  196. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/cc1101.yaml +0 -0
  197. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/cse7766.yaml +0 -0
  198. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/dht.yaml +0 -0
  199. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/ds18b20.yaml +0 -0
  200. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/esp32_camera.yaml +0 -0
  201. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/esp32_rmt_led_strip.yaml +0 -0
  202. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/gpio_input.yaml +0 -0
  203. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/gpio_output.yaml +0 -0
  204. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/hc-sr04.yaml +0 -0
  205. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/hc-sr501.yaml +0 -0
  206. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/hlw8012.yaml +0 -0
  207. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/htu21d.yaml +0 -0
  208. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/hx711.yaml +0 -0
  209. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/i2s_microphone.yaml +0 -0
  210. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/ili9xxx.yaml +0 -0
  211. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/lcd_pcf8574.yaml +0 -0
  212. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/ld2420.yaml +0 -0
  213. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/max31855.yaml +0 -0
  214. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/max7219.yaml +0 -0
  215. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/max98357a.yaml +0 -0
  216. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/mcp23008.yaml +0 -0
  217. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/mcp23017.yaml +0 -0
  218. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/modbus.yaml +0 -0
  219. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/mpu6050.yaml +0 -0
  220. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/mpu6886.yaml +0 -0
  221. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/nextion.yaml +0 -0
  222. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/pcf8574.yaml +0 -0
  223. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/pulse_counter.yaml +0 -0
  224. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/rc522.yaml +0 -0
  225. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/rcwl-0516.yaml +0 -0
  226. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/rdm6300.yaml +0 -0
  227. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/remote_transmitter.yaml +0 -0
  228. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/rf_bridge.yaml +0 -0
  229. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/rotary_encoder.yaml +0 -0
  230. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/rtttl.yaml +0 -0
  231. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/sdm_meter.yaml +0 -0
  232. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/sht3xd.yaml +0 -0
  233. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/ssd1306.yaml +0 -0
  234. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/st7789.yaml +0 -0
  235. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/sx127x.yaml +0 -0
  236. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/tm1638.yaml +0 -0
  237. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/tsl2561.yaml +0 -0
  238. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/tuya.yaml +0 -0
  239. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/tuya_sensor.yaml +0 -0
  240. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/tuya_switch.yaml +0 -0
  241. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/uart_gps.yaml +0 -0
  242. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/uln2003.yaml +0 -0
  243. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/vl53l0x.yaml +0 -0
  244. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/ws2812b.yaml +0 -0
  245. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/components/xpt2046.yaml +0 -0
  246. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/library/modules/oled-knob-13.yaml +0 -0
  247. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/mcp/server.py +0 -0
  248. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/model.py +0 -0
  249. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/recommend/__init__.py +0 -0
  250. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/recommend/recommender.py +0 -0
  251. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/schema/__init__.py +0 -0
  252. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/schema/design.schema.json +0 -0
  253. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/seed.py +0 -0
  254. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/targets/__init__.py +0 -0
  255. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/targets/base.py +0 -0
  256. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/targets/build_backend.py +0 -0
  257. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/targets/esphome.py +0 -0
  258. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/targets/lorawan/__init__.py +0 -0
  259. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/targets/lorawan/__main__.py +0 -0
  260. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/targets/lorawan/api.py +0 -0
  261. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/targets/lorawan/build_local.py +0 -0
  262. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/targets/lorawan/chirpstack.py +0 -0
  263. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/targets/lorawan/codec.py +0 -0
  264. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/targets/lorawan/compile.py +0 -0
  265. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/targets/lorawan/firmware_gen.py +0 -0
  266. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/targets/lorawan/templates/main.cpp.j2 +0 -0
  267. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/targets/lorawan/templates/platformio.ini.j2 +0 -0
  268. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio/validate.py +0 -0
  269. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio.egg-info/SOURCES.txt +0 -0
  270. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio.egg-info/dependency_links.txt +0 -0
  271. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio.egg-info/entry_points.txt +0 -0
  272. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio.egg-info/requires.txt +0 -0
  273. {wirestudio-0.17.2 → wirestudio-0.17.3}/wirestudio.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wirestudio
3
- Version: 0.17.2
3
+ Version: 0.17.3
4
4
  Summary: Agent-driven IoT device design tool that produces ESPHome YAML.
5
5
  Author: wirestudio contributors
6
6
  License: MIT
@@ -43,6 +43,11 @@ class FakeFleetAddon:
43
43
  # ({"id", "run_id", "state", "target", "finished_at"}) to drive the
44
44
  # run-status verdict tests.
45
45
  self.queue_jobs: list[dict] = []
46
+ # run_id -> {"app": bytes | None, "factory": bytes | None}; tests
47
+ # populate this to drive the firmware-artifact passthrough tests.
48
+ # None means "not built" -> the addon 404s (matches the fleet-side
49
+ # contract that 404 = not ready or unavailable).
50
+ self.firmware: dict[str, dict[str, bytes | None]] = {}
46
51
 
47
52
  def transport(self) -> httpx.MockTransport:
48
53
  def handler(req: httpx.Request) -> httpx.Response:
@@ -110,6 +115,26 @@ class FakeFleetAddon:
110
115
  "finished": bool(self.job_logs[run_id]["finished"]),
111
116
  })
112
117
 
118
+ # GET /ui/api/jobs/{run_id}/firmware -> app image
119
+ # GET /ui/api/jobs/{run_id}/firmware/factory -> factory image
120
+ if method == "GET" and path.startswith("/ui/api/jobs/") and (
121
+ path.endswith("/firmware") or path.endswith("/firmware/factory")
122
+ ):
123
+ kind = "factory" if path.endswith("/factory") else "app"
124
+ trim = "/firmware/factory" if kind == "factory" else "/firmware"
125
+ run_id = path[len("/ui/api/jobs/"):-len(trim)]
126
+ slot = self.firmware.get(run_id)
127
+ if slot is None:
128
+ return httpx.Response(404, json={"error": "unknown run_id"})
129
+ data = slot.get(kind)
130
+ if data is None:
131
+ return httpx.Response(404, json={"error": f"no {kind} image"})
132
+ return httpx.Response(
133
+ 200,
134
+ content=data,
135
+ headers={"content-type": "application/octet-stream"},
136
+ )
137
+
113
138
  return httpx.Response(404, json={"error": "not found"})
114
139
 
115
140
  return httpx.MockTransport(handler)
@@ -488,6 +513,115 @@ async def test_fleet_job_log_unknown_run_id_returns_502(monkeypatch, tmp_path):
488
513
  assert r.status_code == 502
489
514
 
490
515
 
516
+ # ---------------------------------------------------------------------------
517
+ # Build-artifact passthrough (fleet -> studio -> browser WebSerial)
518
+ # ---------------------------------------------------------------------------
519
+
520
+ async def test_get_firmware_returns_app_image_bytes():
521
+ addon = FakeFleetAddon()
522
+ addon.firmware["run-1"] = {"app": b"FAKE-APP-IMAGE", "factory": None}
523
+ fc = addon.make_client()
524
+ data = await fc.get_firmware("run-1")
525
+ assert data == b"FAKE-APP-IMAGE"
526
+
527
+
528
+ async def test_get_firmware_factory_uses_factory_subpath():
529
+ addon = FakeFleetAddon()
530
+ addon.firmware["run-1"] = {
531
+ "app": b"APP",
532
+ "factory": b"BOOT+PART+APP",
533
+ }
534
+ fc = addon.make_client()
535
+ data = await fc.get_firmware("run-1", factory=True)
536
+ assert data == b"BOOT+PART+APP"
537
+
538
+
539
+ async def test_get_firmware_unknown_run_id_raises():
540
+ addon = FakeFleetAddon()
541
+ fc = addon.make_client()
542
+ with pytest.raises(FleetUnavailable, match="not available"):
543
+ await fc.get_firmware("nope")
544
+
545
+
546
+ async def test_get_firmware_factory_missing_raises():
547
+ # The build produced an app image but the older ESPHome on the fleet
548
+ # didn't emit firmware-factory.bin -- the addon 404s factory while the
549
+ # app image is fine. The dialog falls back / surfaces the message.
550
+ addon = FakeFleetAddon()
551
+ addon.firmware["run-1"] = {"app": b"APP", "factory": None}
552
+ fc = addon.make_client()
553
+ with pytest.raises(FleetUnavailable, match="not available"):
554
+ await fc.get_firmware("run-1", factory=True)
555
+
556
+
557
+ async def test_get_firmware_unconfigured_raises():
558
+ fc = FleetClient(base_url=None, token=None)
559
+ with pytest.raises(FleetUnavailable, match="missing"):
560
+ await fc.get_firmware("run-1")
561
+
562
+
563
+ async def test_fleet_firmware_endpoint_unconfigured_returns_503(monkeypatch, tmp_path):
564
+ client = _make_client(monkeypatch, tmp_path, addon=None)
565
+ r = client.get("/fleet/jobs/run-1/firmware")
566
+ assert r.status_code == 503
567
+
568
+
569
+ async def test_fleet_firmware_endpoint_round_trips_app_image(monkeypatch, tmp_path):
570
+ addon = FakeFleetAddon()
571
+ addon.firmware["run-7"] = {"app": b"\x01\x02APP", "factory": None}
572
+ client = _make_client(monkeypatch, tmp_path, addon=addon)
573
+ r = client.get("/fleet/jobs/run-7/firmware")
574
+ assert r.status_code == 200
575
+ assert r.headers["content-type"] == "application/octet-stream"
576
+ assert r.headers["content-disposition"] == 'attachment; filename="run-7.bin"'
577
+ assert r.content == b"\x01\x02APP"
578
+
579
+
580
+ async def test_fleet_firmware_endpoint_round_trips_factory_image(monkeypatch, tmp_path):
581
+ addon = FakeFleetAddon()
582
+ addon.firmware["run-7"] = {"app": b"app", "factory": b"\xe9\x00\x00FACTORY"}
583
+ client = _make_client(monkeypatch, tmp_path, addon=addon)
584
+ r = client.get("/fleet/jobs/run-7/firmware?factory=true")
585
+ assert r.status_code == 200
586
+ assert r.headers["content-disposition"] == 'attachment; filename="run-7-factory.bin"'
587
+ assert r.content == b"\xe9\x00\x00FACTORY"
588
+
589
+
590
+ async def test_fleet_firmware_endpoint_unknown_run_id_returns_404(monkeypatch, tmp_path):
591
+ # 404 means "not ready yet / not built" -- the dialog uses this to keep
592
+ # waiting on the compile rather than treating it as a fleet outage (which
593
+ # would be 502).
594
+ addon = FakeFleetAddon()
595
+ client = _make_client(monkeypatch, tmp_path, addon=addon)
596
+ r = client.get("/fleet/jobs/ghost/firmware")
597
+ assert r.status_code == 404
598
+
599
+
600
+ async def test_fleet_firmware_endpoint_addon_unreachable_returns_502(monkeypatch, tmp_path):
601
+ # Addon returns a 5xx (here, simulated via missing handler -> 404 then
602
+ # mapped to "not available"). For a transport-level failure we'd return
603
+ # 502; cover that explicitly by pointing the client at a transport that
604
+ # always 500s.
605
+ def boom_handler(req: httpx.Request) -> httpx.Response:
606
+ if req.url.path.startswith("/ui/api/jobs/") and req.url.path.endswith(
607
+ "/firmware"
608
+ ):
609
+ return httpx.Response(500, json={"error": "internal"})
610
+ return httpx.Response(404)
611
+
612
+ transport = httpx.MockTransport(boom_handler)
613
+ fc = FleetClient(
614
+ base_url="http://broken-fleet.local", token="tok-123", transport=transport,
615
+ )
616
+
617
+ class _BrokenAddon:
618
+ def make_client(self): return fc
619
+
620
+ client = _make_client(monkeypatch, tmp_path, addon=_BrokenAddon())
621
+ r = client.get("/fleet/jobs/run-1/firmware")
622
+ assert r.status_code == 502
623
+
624
+
491
625
  # ---------------------------------------------------------------------------
492
626
  # SSE log relay
493
627
  # ---------------------------------------------------------------------------
@@ -12,6 +12,9 @@ from starlette.routing import Route
12
12
  from wirestudio.mcp.auth import (
13
13
  DEFAULT_TOKEN_PATH,
14
14
  BearerTokenMiddleware,
15
+ TokenManagedError,
16
+ TokenStore,
17
+ load_token_store,
15
18
  resolve_token,
16
19
  )
17
20
 
@@ -34,7 +37,8 @@ def _build_app(token: str) -> Starlette:
34
37
  Route("/public", _ok),
35
38
  ]
36
39
  )
37
- app.add_middleware(BearerTokenMiddleware, token=token)
40
+ store = TokenStore(token=token, path=Path("/unused"), env_managed=True)
41
+ app.add_middleware(BearerTokenMiddleware, store=store)
38
42
  return app
39
43
 
40
44
 
@@ -115,6 +119,43 @@ def test_resolve_token_generates_and_persists(monkeypatch, tmp_path: Path):
115
119
  assert resolve_token(token_path=file_path) == token
116
120
 
117
121
 
122
+ async def test_middleware_reads_rotated_token_without_restart(tmp_path: Path):
123
+ # Rotation updates the live store; the already-mounted middleware must
124
+ # validate against the new token, not the one captured at startup.
125
+ file_path = tmp_path / "mcp-token"
126
+ file_path.write_text("original")
127
+ store = TokenStore(token="original", path=file_path, env_managed=False)
128
+
129
+ app = Starlette(routes=[Route("/mcp", _ok)])
130
+ app.add_middleware(BearerTokenMiddleware, store=store)
131
+ transport = httpx.ASGITransport(app=app)
132
+ async with httpx.AsyncClient(transport=transport, base_url="http://t") as c:
133
+ new_token = store.rotate()
134
+ r_old = await c.get("/mcp", headers={"Authorization": "Bearer original"})
135
+ r_new = await c.get("/mcp", headers={"Authorization": f"Bearer {new_token}"})
136
+ assert r_old.status_code == 401
137
+ assert r_new.status_code == 200
138
+
139
+
140
+ def test_token_store_rotate_persists_new_token(tmp_path: Path):
141
+ file_path = tmp_path / "mcp-token"
142
+ store = load_token_store(token_path=file_path)
143
+ first = store.token
144
+ rotated = store.rotate()
145
+ assert rotated != first
146
+ assert store.token == rotated
147
+ assert file_path.read_text() == rotated
148
+ assert file_path.stat().st_mode & 0o777 == 0o600
149
+
150
+
151
+ def test_token_store_rotate_refuses_env_managed(monkeypatch, tmp_path: Path):
152
+ monkeypatch.setenv("WIRESTUDIO_MCP_TOKEN", "from-env")
153
+ store = load_token_store(token_path=tmp_path / "mcp-token")
154
+ assert store.env_managed is True
155
+ with pytest.raises(TokenManagedError):
156
+ store.rotate()
157
+
158
+
118
159
  def test_default_token_path_is_under_user_config():
119
160
  # Anchored to ~/.config/wirestudio so operators know where to look
120
161
  # without grepping the source.
@@ -1,3 +1,3 @@
1
1
  """wirestudio: design.json -> ESPHome YAML + ASCII diagram."""
2
2
 
3
- __version__ = "0.17.2"
3
+ __version__ = "0.17.3"
@@ -22,7 +22,12 @@ from slowapi.errors import RateLimitExceeded
22
22
  import os
23
23
  from pydantic import ValidationError
24
24
 
25
- from wirestudio.mcp import BearerTokenMiddleware, build_mcp_server, resolve_token
25
+ from wirestudio.mcp import (
26
+ BearerTokenMiddleware,
27
+ TokenManagedError,
28
+ build_mcp_server,
29
+ load_token_store,
30
+ )
26
31
 
27
32
  from wirestudio import __version__
28
33
  from wirestudio.agent.agent import is_available as agent_available, run_turn, stream_turn_events
@@ -42,6 +47,7 @@ from wirestudio.api.schemas import (
42
47
  InventoryCheckRequest,
43
48
  InventoryCheckResponse,
44
49
  InventoryEntryModel,
50
+ McpTokenResponse,
45
51
  ModuleSummary,
46
52
  FleetJobLogResponse,
47
53
  FleetJobStatus,
@@ -1212,6 +1218,53 @@ def create_app(
1212
1218
  headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
1213
1219
  )
1214
1220
 
1221
+ @app.get("/fleet/jobs/{run_id}/firmware", tags=["fleet"])
1222
+ async def fleet_job_firmware(run_id: str, factory: bool = False) -> Response:
1223
+ """Passthrough for the fleet's compiled-firmware artifact -- the
1224
+ studio fetches it server-side with FLEET_TOKEN and streams the
1225
+ bytes to the browser's WebSerial flasher (no token in the browser).
1226
+
1227
+ ``?factory=true`` returns the merged bootloader+partitions+app
1228
+ image for flashing a blank board at offset 0x0 (paired with
1229
+ ``eraseAll: true`` on the browser side); default is the app image
1230
+ for an NVS-preserving re-flash. Mirrors the standalone path's
1231
+ ``/lorawan/firmware/{cache_key}[/factory]`` so the WebSerial flow
1232
+ on the external-component path can reuse ``lib/flash.ts`` unchanged.
1233
+
1234
+ Returns 404 when the run hasn't finished, has aged out of the
1235
+ addon's tracking, or the build didn't produce the requested image
1236
+ type; 503 when fleet credentials are missing; 502 when the addon
1237
+ is reachable but the call failed (e.g. the upstream artifact
1238
+ endpoint isn't implemented yet -- see
1239
+ ``docs/lorawan/fleet-firmware-flash.md``).
1240
+ """
1241
+ fc = make_fleet()
1242
+ if not fc.is_configured():
1243
+ raise HTTPException(
1244
+ status_code=503,
1245
+ detail="fleet not configured (set FLEET_URL and FLEET_TOKEN)",
1246
+ )
1247
+ try:
1248
+ data = await fc.get_firmware(run_id, factory=factory)
1249
+ except FleetUnavailable as e:
1250
+ # The client raises FleetUnavailable on both 404 (firmware not
1251
+ # available -> the dialog stops the flash flow with a clear
1252
+ # message) and transport failures (the addon being down).
1253
+ # Preserve the 404 vs 502 distinction the addon emitted so the
1254
+ # browser can tell "not ready yet" from "fleet is down".
1255
+ msg = str(e)
1256
+ if "not available" in msg:
1257
+ raise HTTPException(status_code=404, detail=msg) from e
1258
+ raise HTTPException(status_code=502, detail=msg) from e
1259
+ suffix = "-factory" if factory else ""
1260
+ return Response(
1261
+ content=data,
1262
+ media_type="application/octet-stream",
1263
+ headers={
1264
+ "Content-Disposition": f'attachment; filename="{run_id}{suffix}.bin"',
1265
+ },
1266
+ )
1267
+
1215
1268
  @app.get("/agent/status", tags=["agent"])
1216
1269
  def agent_status() -> dict:
1217
1270
  ok, reason = agent_available()
@@ -1307,11 +1360,32 @@ def create_app(
1307
1360
  h.strip() for h in allowed_hosts.split(",") if h.strip()
1308
1361
  ]
1309
1362
  token_path_env = os.environ.get("WIRESTUDIO_MCP_TOKEN_PATH")
1310
- token = resolve_token(
1363
+ token_store = load_token_store(
1311
1364
  token_path=Path(token_path_env) if token_path_env else None,
1312
1365
  )
1366
+
1367
+ # Token-management endpoints for the web UI. Registered before the
1368
+ # catch-all mount below so FastAPI serves them directly -- they sit on
1369
+ # the unauthenticated API surface (the bearer token only gates /mcp),
1370
+ # which is required so the UI can show the token to a user who doesn't
1371
+ # have it yet.
1372
+ @app.get("/mcp/token", response_model=McpTokenResponse, tags=["mcp"])
1373
+ def get_mcp_token() -> McpTokenResponse:
1374
+ return McpTokenResponse(
1375
+ token=token_store.token,
1376
+ managed="env" if token_store.env_managed else "file",
1377
+ )
1378
+
1379
+ @app.post("/mcp/token/rotate", response_model=McpTokenResponse, tags=["mcp"])
1380
+ def rotate_mcp_token() -> McpTokenResponse:
1381
+ try:
1382
+ new_token = token_store.rotate()
1383
+ except TokenManagedError as e:
1384
+ raise HTTPException(status_code=409, detail=str(e)) from e
1385
+ return McpTokenResponse(token=new_token, managed="file")
1386
+
1313
1387
  mcp_app = mcp_server.streamable_http_app()
1314
- mcp_app.add_middleware(BearerTokenMiddleware, token=token)
1388
+ mcp_app.add_middleware(BearerTokenMiddleware, store=token_store)
1315
1389
  app.mount("/", mcp_app)
1316
1390
 
1317
1391
  return app
@@ -3,7 +3,7 @@
3
3
  """
4
4
  from __future__ import annotations
5
5
 
6
- from typing import Optional
6
+ from typing import Literal, Optional
7
7
 
8
8
  from pydantic import BaseModel, ConfigDict, Field
9
9
 
@@ -12,6 +12,14 @@ class _S(BaseModel):
12
12
  model_config = ConfigDict(extra="forbid")
13
13
 
14
14
 
15
+ class McpTokenResponse(_S):
16
+ token: str = Field(description="Current MCP bearer token.")
17
+ managed: Literal["file", "env"] = Field(
18
+ description="Token source: 'file' (rotatable here) or 'env' (set via "
19
+ "WIRESTUDIO_MCP_TOKEN, read-only)."
20
+ )
21
+
22
+
15
23
  class BoardSummary(_S):
16
24
  id: str = Field(description="Board library id, e.g. 'esp32-devkitc-v4'.")
17
25
  name: str = Field(description="Human-readable board name.")
@@ -282,6 +282,50 @@ class FleetClient:
282
282
  ]
283
283
  return RunStatus(run_id=run_id, verdict=_verdict(jobs), jobs=jobs)
284
284
 
285
+ # ------------------------------------------------------------------
286
+ # Build-artifact fetch
287
+ # ------------------------------------------------------------------
288
+
289
+ async def get_firmware(
290
+ self, run_id: str, *, factory: bool = False
291
+ ) -> bytes:
292
+ """Fetch the compiled firmware bytes for a finished build.
293
+
294
+ Mirrors the addon's artifact endpoint at
295
+ ``GET /ui/api/jobs/{run_id}/firmware[/factory]`` (the upstream
296
+ half of the headless-device flash story -- see
297
+ ``docs/lorawan/fleet-firmware-flash.md``). The browser never holds
298
+ ``FLEET_TOKEN``; this is the studio-side passthrough that fetches
299
+ with the server token and re-streams the bytes to the WebSerial
300
+ flasher via the matching studio route.
301
+
302
+ ``factory=False`` (default) fetches the app image (re-flash that
303
+ preserves NVS); ``factory=True`` fetches the merged factory image
304
+ for blank-board flashing at offset 0x0. 404 -> ``FleetUnavailable``
305
+ the same way ``get_job_log`` handles unknown run_ids, so the UI
306
+ can stop polling.
307
+
308
+ The upstream endpoint does not exist yet at the time of writing
309
+ (it's the scoped fleet half); calls will 502 via the matching
310
+ studio route until the addon ships it. That gates the dialog's
311
+ "Flash via WebSerial" button on the upstream rollout without
312
+ changing this client surface later.
313
+ """
314
+ if not self.is_configured():
315
+ raise FleetUnavailable("FLEET_URL or FLEET_TOKEN missing")
316
+ suffix = "/factory" if factory else ""
317
+ async with self._client() as c:
318
+ resp = await c.get(f"/ui/api/jobs/{run_id}/firmware{suffix}")
319
+ if resp.status_code == 404:
320
+ raise FleetUnavailable(
321
+ f"firmware artifact not available for run_id {run_id!r}"
322
+ )
323
+ if resp.status_code >= 400:
324
+ raise FleetUnavailable(
325
+ f"firmware fetch failed: http {resp.status_code} {resp.text}"
326
+ )
327
+ return resp.content
328
+
285
329
  # ------------------------------------------------------------------
286
330
  # Internals
287
331
  # ------------------------------------------------------------------
@@ -8,6 +8,9 @@ into the existing FastAPI app at `/mcp` over the Streamable HTTP transport.
8
8
  from wirestudio.mcp.auth import (
9
9
  DEFAULT_TOKEN_PATH,
10
10
  BearerTokenMiddleware,
11
+ TokenManagedError,
12
+ TokenStore,
13
+ load_token_store,
11
14
  resolve_token,
12
15
  )
13
16
  from wirestudio.mcp.server import build_mcp_server
@@ -15,6 +18,9 @@ from wirestudio.mcp.server import build_mcp_server
15
18
  __all__ = [
16
19
  "BearerTokenMiddleware",
17
20
  "DEFAULT_TOKEN_PATH",
21
+ "TokenManagedError",
22
+ "TokenStore",
18
23
  "build_mcp_server",
24
+ "load_token_store",
19
25
  "resolve_token",
20
26
  ]
@@ -21,32 +21,83 @@ DEFAULT_TOKEN_PATH = Path(os.path.expanduser("~/.config/wirestudio/mcp-token"))
21
21
  ENV_VAR = "WIRESTUDIO_MCP_TOKEN"
22
22
 
23
23
 
24
- def resolve_token(
24
+ class TokenManagedError(RuntimeError):
25
+ """Raised when rotation is attempted on an externally-managed token.
26
+
27
+ When the token comes from `WIRESTUDIO_MCP_TOKEN` we can't rewrite it -- the
28
+ operator owns it (k8s Secret / sops). Rotating would only diverge the live
29
+ value from the secret, so we refuse.
30
+ """
31
+
32
+
33
+ def _generate_and_persist(path: Path) -> str:
34
+ token = secrets.token_urlsafe(32)
35
+ path.parent.mkdir(parents=True, exist_ok=True)
36
+ path.write_text(token)
37
+ path.chmod(0o600)
38
+ return token
39
+
40
+
41
+ class TokenStore:
42
+ """Holds the live MCP bearer token so rotation takes effect without a restart.
43
+
44
+ `BearerTokenMiddleware` reads `.token` on every request rather than
45
+ capturing the value at construction time. `env_managed` is True when the
46
+ token came from the env var, in which case `rotate()` raises.
47
+ """
48
+
49
+ def __init__(self, *, token: str, path: Path, env_managed: bool) -> None:
50
+ self._token = token
51
+ self._path = path
52
+ self.env_managed = env_managed
53
+
54
+ @property
55
+ def token(self) -> str:
56
+ return self._token
57
+
58
+ def rotate(self) -> str:
59
+ if self.env_managed:
60
+ raise TokenManagedError(
61
+ f"token is set via {ENV_VAR}; rotate it by updating that secret, not here"
62
+ )
63
+ self._token = _generate_and_persist(self._path)
64
+ logger.info("Rotated MCP token; persisted to %s", self._path)
65
+ return self._token
66
+
67
+
68
+ def load_token_store(
25
69
  *,
26
70
  env_var: str = ENV_VAR,
27
71
  token_path: Path | None = None,
28
- ) -> str:
29
- """Return the MCP bearer token, generating + persisting one on first call.
72
+ ) -> TokenStore:
73
+ """Resolve the MCP bearer token into a mutable store.
30
74
 
31
75
  The env var wins if set (operators using k8s Secrets / sops set this and
32
76
  the token-file path is ignored). Otherwise the persisted file is read.
33
77
  Otherwise a fresh token is generated, persisted with mode 0600, and the
34
78
  path is logged at INFO so the operator knows where to copy it from.
35
79
  """
80
+ path = token_path or DEFAULT_TOKEN_PATH
81
+
36
82
  env_token = os.environ.get(env_var)
37
83
  if env_token:
38
- return env_token.strip()
84
+ return TokenStore(token=env_token.strip(), path=path, env_managed=True)
39
85
 
40
- path = token_path or DEFAULT_TOKEN_PATH
41
86
  if path.exists():
42
- return path.read_text().strip()
87
+ return TokenStore(token=path.read_text().strip(), path=path, env_managed=False)
43
88
 
44
- token = secrets.token_urlsafe(32)
45
- path.parent.mkdir(parents=True, exist_ok=True)
46
- path.write_text(token)
47
- path.chmod(0o600)
89
+ token = _generate_and_persist(path)
48
90
  logger.info("Generated MCP token; copy it from %s", path)
49
- return token
91
+ return TokenStore(token=token, path=path, env_managed=False)
92
+
93
+
94
+ def resolve_token(
95
+ *,
96
+ env_var: str = ENV_VAR,
97
+ token_path: Path | None = None,
98
+ ) -> str:
99
+ """Return the current MCP bearer token. See `load_token_store`."""
100
+ return load_token_store(env_var=env_var, token_path=token_path).token
50
101
 
51
102
 
52
103
  class BearerTokenMiddleware:
@@ -59,9 +110,9 @@ class BearerTokenMiddleware:
59
110
  only gates the MCP endpoint, not the rest of the API.
60
111
  """
61
112
 
62
- def __init__(self, app: ASGIApp, *, token: str, path_prefix: str = "/mcp") -> None:
113
+ def __init__(self, app: ASGIApp, *, store: TokenStore, path_prefix: str = "/mcp") -> None:
63
114
  self.app = app
64
- self._token = token
115
+ self._store = store
65
116
  # Regex match: prefix as a path component (preceded by "/" or start,
66
117
  # followed by "/" or end). We can't compare scope[path] literally
67
118
  # because Starlette's nested Mount('/') doesn't strip path
@@ -88,7 +139,7 @@ class BearerTokenMiddleware:
88
139
  auth = headers.get(b"authorization", b"").decode("latin-1", errors="ignore")
89
140
  prefix = "Bearer "
90
141
  if not auth.startswith(prefix) or not secrets.compare_digest(
91
- auth[len(prefix):], self._token
142
+ auth[len(prefix):], self._store.token
92
143
  ):
93
144
  await _send_401(send)
94
145
  return
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wirestudio
3
- Version: 0.17.2
3
+ Version: 0.17.3
4
4
  Summary: Agent-driven IoT device design tool that produces ESPHome YAML.
5
5
  Author: wirestudio contributors
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes