voodoo-framework 2.5.2__tar.gz → 2.6.0__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 (244) hide show
  1. {voodoo_framework-2.5.2/src/voodoo_framework.egg-info → voodoo_framework-2.6.0}/PKG-INFO +13 -1
  2. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/README.md +10 -0
  3. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/pyproject.toml +3 -0
  4. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/__init__.py +1 -1
  5. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/config.py +82 -0
  6. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/core/app.py +47 -0
  7. voodoo_framework-2.6.0/src/voodoo/edge/__init__.py +122 -0
  8. voodoo_framework-2.6.0/src/voodoo/edge/auth.py +243 -0
  9. voodoo_framework-2.6.0/src/voodoo/edge/errors.py +181 -0
  10. voodoo_framework-2.6.0/src/voodoo/edge/gateway.py +556 -0
  11. voodoo_framework-2.6.0/src/voodoo/edge/http.py +316 -0
  12. voodoo_framework-2.6.0/src/voodoo/edge/models.py +309 -0
  13. voodoo_framework-2.6.0/src/voodoo/edge/mqtt.py +239 -0
  14. voodoo_framework-2.6.0/src/voodoo/edge/protocol.py +282 -0
  15. voodoo_framework-2.6.0/src/voodoo/edge/simulator.py +341 -0
  16. voodoo_framework-2.6.0/src/voodoo/edge/store.py +835 -0
  17. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0/src/voodoo_framework.egg-info}/PKG-INFO +13 -1
  18. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo_framework.egg-info/SOURCES.txt +14 -0
  19. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo_framework.egg-info/requires.txt +3 -0
  20. voodoo_framework-2.6.0/tests/test_edge_device.py +325 -0
  21. voodoo_framework-2.6.0/tests/test_edge_gateway.py +417 -0
  22. voodoo_framework-2.6.0/tests/test_edge_http.py +293 -0
  23. voodoo_framework-2.6.0/tests/test_edge_protocol.py +159 -0
  24. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/LICENSE +0 -0
  25. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/setup.cfg +0 -0
  26. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/adapters/__init__.py +0 -0
  27. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/adapters/capabilities.py +0 -0
  28. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/adapters/registry.py +0 -0
  29. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/adapters/tailwind.py +0 -0
  30. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/adapters/voodoo_css.py +0 -0
  31. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/agent.py +0 -0
  32. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/agents/__init__.py +0 -0
  33. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/agents/models.py +0 -0
  34. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/agents/registry.py +0 -0
  35. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ai/__init__.py +0 -0
  36. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ai/agent.py +0 -0
  37. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ai/providers/__init__.py +0 -0
  38. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ai/providers/anthropic.py +0 -0
  39. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ai/providers/gemini.py +0 -0
  40. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ai/providers/mock.py +0 -0
  41. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ai/providers/ollama.py +0 -0
  42. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ai/providers/openai.py +0 -0
  43. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ai/tools/__init__.py +0 -0
  44. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ai/tools/registry.py +0 -0
  45. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/api.py +0 -0
  46. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/auth/__init__.py +0 -0
  47. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/auth/cookies.py +0 -0
  48. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/auth/guards.py +0 -0
  49. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/auth/jwt.py +0 -0
  50. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/auth/middleware.py +0 -0
  51. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/auth/passwords.py +0 -0
  52. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/auth/user.py +0 -0
  53. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/__init__.py +0 -0
  54. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/__main__.py +0 -0
  55. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/agents.py +0 -0
  56. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/ai.py +0 -0
  57. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/approvals.py +0 -0
  58. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/auth.py +0 -0
  59. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/create.py +0 -0
  60. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/dev.py +0 -0
  61. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/doctor.py +0 -0
  62. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/executions.py +0 -0
  63. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/generate.py +0 -0
  64. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/inspect.py +0 -0
  65. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/new.py +0 -0
  66. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/objects.py +0 -0
  67. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/protocol.py +0 -0
  68. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/recover.py +0 -0
  69. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/routes.py +0 -0
  70. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/scaffolding.py +0 -0
  71. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/schedules.py +0 -0
  72. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/status.py +0 -0
  73. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/tasks.py +0 -0
  74. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/terminal.py +0 -0
  75. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/theme.py +0 -0
  76. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/version.py +0 -0
  77. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/workers_cmd.py +0 -0
  78. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/components.py +0 -0
  79. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/core/__init__.py +0 -0
  80. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/core/errors.py +0 -0
  81. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/core/events.py +0 -0
  82. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/core/render.py +0 -0
  83. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/core/routing.py +0 -0
  84. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/core/sitemap.py +0 -0
  85. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/core/state.py +0 -0
  86. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/data/__init__.py +0 -0
  87. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/data/base.py +0 -0
  88. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/data/model.py +0 -0
  89. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/i18n.py +0 -0
  90. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/mcp/__init__.py +0 -0
  91. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/mcp/client.py +0 -0
  92. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/memory/__init__.py +0 -0
  93. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/memory/interfaces.py +0 -0
  94. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/memory/sqlite.py +0 -0
  95. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/mesh/__init__.py +0 -0
  96. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/mesh/client.py +0 -0
  97. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/mesh/event_bus.py +0 -0
  98. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/primitives/__init__.py +0 -0
  99. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/primitives/capability.py +0 -0
  100. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/primitives/compute.py +0 -0
  101. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/primitives/constraint.py +0 -0
  102. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/primitives/effect.py +0 -0
  103. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/primitives/intent.py +0 -0
  104. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/primitives/resource.py +0 -0
  105. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/primitives/state.py +0 -0
  106. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/primitives/time.py +0 -0
  107. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/protocol/__init__.py +0 -0
  108. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/protocol/export.py +0 -0
  109. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/protocol/schemas.py +0 -0
  110. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/py.typed +0 -0
  111. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/queue.py +0 -0
  112. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/routing/api.py +0 -0
  113. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/routing/pages.py +0 -0
  114. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/__init__.py +0 -0
  115. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/adaptive.py +0 -0
  116. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/capability.py +0 -0
  117. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/constraint.py +0 -0
  118. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/context.py +0 -0
  119. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/engine.py +0 -0
  120. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/errors.py +0 -0
  121. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/execution.py +0 -0
  122. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/graph.py +0 -0
  123. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/human.py +0 -0
  124. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/persistence.py +0 -0
  125. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/planner.py +0 -0
  126. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/scheduler.py +0 -0
  127. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/task.py +0 -0
  128. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/workflow.py +0 -0
  129. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/schedule.py +0 -0
  130. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/security/__init__.py +0 -0
  131. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/security/cors.py +0 -0
  132. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/security/csrf.py +0 -0
  133. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/security/headers.py +0 -0
  134. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/security/passwords.py +0 -0
  135. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/security/rate_limit.py +0 -0
  136. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/security/redaction.py +0 -0
  137. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/security/secrets.py +0 -0
  138. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/seo.py +0 -0
  139. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/static/client.js +0 -0
  140. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/status.py +0 -0
  141. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/__init__.py +0 -0
  142. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/cache/__init__.py +0 -0
  143. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/cache/interfaces.py +0 -0
  144. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/cache/memory.py +0 -0
  145. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/cache/redis.py +0 -0
  146. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/database/__init__.py +0 -0
  147. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/database/interfaces.py +0 -0
  148. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/database/postgres.py +0 -0
  149. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/database/sqlite.py +0 -0
  150. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/events/__init__.py +0 -0
  151. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/events/interfaces.py +0 -0
  152. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/events/local.py +0 -0
  153. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/events/postgres.py +0 -0
  154. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/events/sqlite.py +0 -0
  155. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/execution/__init__.py +0 -0
  156. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/execution/migrations.py +0 -0
  157. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/execution/postgres.py +0 -0
  158. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/execution/sqlite.py +0 -0
  159. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/manager.py +0 -0
  160. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/objects/__init__.py +0 -0
  161. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/objects/interfaces.py +0 -0
  162. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/objects/local.py +0 -0
  163. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/objects/s3.py +0 -0
  164. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/queue/__init__.py +0 -0
  165. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/queue/interfaces.py +0 -0
  166. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/queue/memory.py +0 -0
  167. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/queue/postgres.py +0 -0
  168. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/queue/redis.py +0 -0
  169. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/queue/sqlite.py +0 -0
  170. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/scheduler/__init__.py +0 -0
  171. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/scheduler/sqlite.py +0 -0
  172. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/telemetry/__init__.py +0 -0
  173. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/telemetry/api.py +0 -0
  174. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/telemetry/middleware.py +0 -0
  175. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/telemetry/otlp.py +0 -0
  176. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/telemetry/store.py +0 -0
  177. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/theme.py +0 -0
  178. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/tools/__init__.py +0 -0
  179. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/tools/registry.py +0 -0
  180. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ui/__init__.py +0 -0
  181. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ui/component.py +0 -0
  182. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ui/events.py +0 -0
  183. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ui/library.py +0 -0
  184. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ui/markdown.py +0 -0
  185. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ui/rendering.py +0 -0
  186. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ui/state.py +0 -0
  187. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ui/styles/__init__.py +0 -0
  188. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ui/styles/presets.py +0 -0
  189. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ui/styles/theme.py +0 -0
  190. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/workers/__init__.py +0 -0
  191. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/workers/queue.py +0 -0
  192. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo_framework.egg-info/dependency_links.txt +0 -0
  193. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo_framework.egg-info/entry_points.txt +0 -0
  194. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo_framework.egg-info/top_level.txt +0 -0
  195. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_agent.py +0 -0
  196. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_agent_registry.py +0 -0
  197. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_agent_runtime.py +0 -0
  198. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_app.py +0 -0
  199. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_auth.py +0 -0
  200. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_capability_security.py +0 -0
  201. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_checkpoint_resume.py +0 -0
  202. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_chrome.py +0 -0
  203. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_cli.py +0 -0
  204. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_cli_inspect.py +0 -0
  205. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_components.py +0 -0
  206. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_config.py +0 -0
  207. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_contract_api.py +0 -0
  208. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_create.py +0 -0
  209. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_data.py +0 -0
  210. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_design_system.py +0 -0
  211. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_durable_hitl.py +0 -0
  212. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_events.py +0 -0
  213. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_execution_sqlite_store.py +0 -0
  214. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_http_runtime.py +0 -0
  215. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_human.py +0 -0
  216. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_i18n.py +0 -0
  217. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_integration.py +0 -0
  218. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_mcp.py +0 -0
  219. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_mcp_runtime.py +0 -0
  220. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_memory.py +0 -0
  221. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_mesh.py +0 -0
  222. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_mesh_event_bus.py +0 -0
  223. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_model.py +0 -0
  224. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_object_store.py +0 -0
  225. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_observability.py +0 -0
  226. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_persistence.py +0 -0
  227. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_planner_adaptive.py +0 -0
  228. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_primitives.py +0 -0
  229. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_protocol.py +0 -0
  230. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_provider_switching.py +0 -0
  231. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_providers.py +0 -0
  232. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_queue.py +0 -0
  233. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_reactive.py +0 -0
  234. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_runtime.py +0 -0
  235. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_scheduler.py +0 -0
  236. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_security.py +0 -0
  237. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_seo.py +0 -0
  238. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_state.py +0 -0
  239. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_theme.py +0 -0
  240. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_theme_presets.py +0 -0
  241. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_tools.py +0 -0
  242. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_ui.py +0 -0
  243. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_websocket.py +0 -0
  244. {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_workers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voodoo-framework
3
- Version: 2.5.2
3
+ Version: 2.6.0
4
4
  Summary: The programmable runtime for adaptive applications and operational systems — web, APIs, agents, workers, human workflows, distributed systems, and physical systems in one runtime.
5
5
  Author-email: Helder Perez <contact@helderperez.com>
6
6
  Maintainer-email: Helder Perez <contact@helderperez.com>
@@ -59,6 +59,8 @@ Requires-Dist: boto3>=1.34; extra == "s3"
59
59
  Requires-Dist: botocore>=1.34; extra == "s3"
60
60
  Provides-Extra: redis
61
61
  Requires-Dist: redis>=5.0; extra == "redis"
62
+ Provides-Extra: edge
63
+ Requires-Dist: paho-mqtt>=2.0; extra == "edge"
62
64
  Provides-Extra: otel
63
65
  Requires-Dist: opentelemetry-api>=1.20; extra == "otel"
64
66
  Requires-Dist: opentelemetry-sdk>=1.20; extra == "otel"
@@ -232,6 +234,7 @@ if __name__ == "__main__":
232
234
  | **Observability everywhere** | Correlation IDs and telemetry built into every subsystem |
233
235
  | **Unified runtime engine** | Every operation (HTTP, Agent, Tool, MCP, Worker, Human, Event) produces an `Execution` record with full traceability |
234
236
  | **Human-in-the-Loop** | `ask_human()` + `approve()`/`deny()` — humans as compute participants, not afterthoughts |
237
+ | **Physical devices as Entities** | `voodoo-edge/v1` protocol + Device Gateway over HTTP/MQTT — ESP32s, robots, and sensors join the same execution model as agents and workers |
235
238
  | **Adaptive execution** | Planner resolves capabilities to compute participants; supervisor steers with retry, fallback, budget control |
236
239
  | **Durable by default** | Tasks, executions, schedules, and events survive process restarts — backed by SQLite out of the box |
237
240
  | **Zero-config runtime** | `voodoo create` → `voodoo dev` → working app. No build step. Add `voodoo.toml` when you need configuration |
@@ -284,6 +287,7 @@ and [`ARCHITECTURE.md`](ARCHITECTURE.md) for the implementation model.
284
287
  - **Auth** — JWT tokens, API keys, session cookies, RBAC route guards
285
288
  - **Workers** — `@task` decorator with retries, timeout, and telemetry spans
286
289
  - **Voodoo Mesh** — Realtime event bus with local + remote boundaries
290
+ - **Edge Devices** — Device Gateway (`voodoo-edge/v1` over HTTP & MQTT): devices emit events, trigger Executions, receive Effects — see [`docs/edge/overview.md`](docs/edge/overview.md)
287
291
 
288
292
  ### Runtime & Infrastructure
289
293
  - **Runtime Engine** — Unified `ExecutionEngine` producing `Execution` records for every operation
@@ -437,6 +441,14 @@ voodoo auth secret-key
437
441
  - [Mesh](docs/mesh.md)
438
442
  - [Workers](docs/workers.md)
439
443
 
444
+ ### Edge Devices
445
+ - [Edge Overview](docs/edge/overview.md)
446
+ - [Edge Protocol v1](docs/edge/protocol.md)
447
+ - [HTTP Transport](docs/edge/http.md)
448
+ - [MQTT Transport](docs/edge/mqtt.md)
449
+ - [Device Security](docs/edge/security.md)
450
+ - [Reliability](docs/edge/reliability.md)
451
+
440
452
  ### Runtime & Operations
441
453
  - [Runtime Engine](docs/runtime.md)
442
454
  - [Planner & Adaptive Runtime](docs/adaptive.md)
@@ -165,6 +165,7 @@ if __name__ == "__main__":
165
165
  | **Observability everywhere** | Correlation IDs and telemetry built into every subsystem |
166
166
  | **Unified runtime engine** | Every operation (HTTP, Agent, Tool, MCP, Worker, Human, Event) produces an `Execution` record with full traceability |
167
167
  | **Human-in-the-Loop** | `ask_human()` + `approve()`/`deny()` — humans as compute participants, not afterthoughts |
168
+ | **Physical devices as Entities** | `voodoo-edge/v1` protocol + Device Gateway over HTTP/MQTT — ESP32s, robots, and sensors join the same execution model as agents and workers |
168
169
  | **Adaptive execution** | Planner resolves capabilities to compute participants; supervisor steers with retry, fallback, budget control |
169
170
  | **Durable by default** | Tasks, executions, schedules, and events survive process restarts — backed by SQLite out of the box |
170
171
  | **Zero-config runtime** | `voodoo create` → `voodoo dev` → working app. No build step. Add `voodoo.toml` when you need configuration |
@@ -217,6 +218,7 @@ and [`ARCHITECTURE.md`](ARCHITECTURE.md) for the implementation model.
217
218
  - **Auth** — JWT tokens, API keys, session cookies, RBAC route guards
218
219
  - **Workers** — `@task` decorator with retries, timeout, and telemetry spans
219
220
  - **Voodoo Mesh** — Realtime event bus with local + remote boundaries
221
+ - **Edge Devices** — Device Gateway (`voodoo-edge/v1` over HTTP & MQTT): devices emit events, trigger Executions, receive Effects — see [`docs/edge/overview.md`](docs/edge/overview.md)
220
222
 
221
223
  ### Runtime & Infrastructure
222
224
  - **Runtime Engine** — Unified `ExecutionEngine` producing `Execution` records for every operation
@@ -370,6 +372,14 @@ voodoo auth secret-key
370
372
  - [Mesh](docs/mesh.md)
371
373
  - [Workers](docs/workers.md)
372
374
 
375
+ ### Edge Devices
376
+ - [Edge Overview](docs/edge/overview.md)
377
+ - [Edge Protocol v1](docs/edge/protocol.md)
378
+ - [HTTP Transport](docs/edge/http.md)
379
+ - [MQTT Transport](docs/edge/mqtt.md)
380
+ - [Device Security](docs/edge/security.md)
381
+ - [Reliability](docs/edge/reliability.md)
382
+
373
383
  ### Runtime & Operations
374
384
  - [Runtime Engine](docs/runtime.md)
375
385
  - [Planner & Adaptive Runtime](docs/adaptive.md)
@@ -93,6 +93,9 @@ s3 = [
93
93
  redis = [
94
94
  "redis>=5.0",
95
95
  ]
96
+ edge = [
97
+ "paho-mqtt>=2.0",
98
+ ]
96
99
  otel = [
97
100
  "opentelemetry-api>=1.20",
98
101
  "opentelemetry-sdk>=1.20",
@@ -102,7 +102,7 @@ from .ui.styles import (
102
102
  from .ui.styles.theme import Theme, ThemeColors, create_theme
103
103
  from .workers import task
104
104
 
105
- __version__ = "2.5.2"
105
+ __version__ = "2.6.0"
106
106
 
107
107
  __all__ = [
108
108
  # Core runtime
@@ -255,6 +255,37 @@ class AIConfig(BaseModel):
255
255
  aliases: dict[str, str] = Field(default_factory=dict)
256
256
 
257
257
 
258
+ class EdgeConfig(BaseModel):
259
+ """Edge device gateway configuration (``edge:`` block) — Sprint 23.
260
+
261
+ Edge is disabled by default: users who never enable it pay zero
262
+ overhead and need no MQTT broker, device tables, or gateway
263
+ (EDGE §68 — backwards compatibility).
264
+ """
265
+
266
+ enabled: bool = False
267
+ http_enabled: bool = True
268
+ mqtt_enabled: bool = False
269
+ # Optional separate listener for the edge gateway; when empty the
270
+ # edge routes mount onto the main app instead.
271
+ http_host: str = ""
272
+ http_port: int = 0
273
+ # MQTT broker connection (EDGE §35, §38).
274
+ mqtt_broker_url: str = ""
275
+ mqtt_port: int = 1883
276
+ mqtt_tls: bool = False
277
+ mqtt_username: str = ""
278
+ mqtt_password: str = ""
279
+ mqtt_client_id: str = "voodoo-runtime"
280
+ mqtt_keepalive: int = 60
281
+ mqtt_qos: int = 1
282
+ # Resource limits (EDGE §76).
283
+ max_message_size: int = 262144 # 256 KiB
284
+ max_state_size: int = 65536 # 64 KiB
285
+ max_pending_effects: int = 1000
286
+ heartbeat_interval: int = 30
287
+
288
+
258
289
  class VoodooConfig(BaseModel):
259
290
  """Core configuration for the Voodoo framework."""
260
291
 
@@ -276,6 +307,7 @@ class VoodooConfig(BaseModel):
276
307
  cache: CacheConfig = Field(default_factory=CacheConfig)
277
308
  models: ModelsConfig = Field(default_factory=ModelsConfig)
278
309
  ai: AIConfig = Field(default_factory=AIConfig)
310
+ edge: EdgeConfig = Field(default_factory=EdgeConfig)
279
311
 
280
312
  seo: SEOConfig = Field(default_factory=SEOConfig)
281
313
  auth: AuthConfig = Field(default_factory=AuthConfig)
@@ -458,6 +490,54 @@ def _build_ai_config(file_data: dict[str, Any]) -> AIConfig:
458
490
  )
459
491
 
460
492
 
493
+ def _build_edge_config(file_data: dict[str, Any]) -> EdgeConfig:
494
+ """Merge file ``edge:`` block with ``VOODOO_EDGE_*``/``MQTT_*`` env vars."""
495
+ edge_data = file_data.get("edge") or {}
496
+ if not isinstance(edge_data, dict):
497
+ edge_data = {"enabled": bool(edge_data)}
498
+
499
+ def _flag(key: str, env: str) -> bool | None:
500
+ if key in edge_data:
501
+ return bool(edge_data[key])
502
+ val = _env_flag(env)
503
+ return val
504
+
505
+ enabled = _flag("enabled", "VOODOO_EDGE_ENABLED")
506
+ mqtt = edge_data.get("mqtt") or {}
507
+ if not isinstance(mqtt, dict):
508
+ mqtt = {}
509
+
510
+ kwargs: dict[str, Any] = {
511
+ "enabled": enabled if enabled is not None else False,
512
+ "http_enabled": (_flag("http_enabled", "VOODOO_EDGE_HTTP_ENABLED") or False)
513
+ if "http_enabled" in edge_data or "VOODOO_EDGE_HTTP_ENABLED" in os.environ
514
+ else True,
515
+ "mqtt_enabled": (_flag("mqtt_enabled", "VOODOO_EDGE_MQTT_ENABLED") or False)
516
+ if "mqtt_enabled" in edge_data or "VOODOO_EDGE_MQTT_ENABLED" in os.environ
517
+ else (bool(mqtt) or "MQTT_BROKER_URL" in os.environ),
518
+ "mqtt_broker_url": mqtt.get("broker_url") or os.getenv("MQTT_BROKER_URL", ""),
519
+ "mqtt_port": int(mqtt.get("port") or os.getenv("MQTT_PORT", "1883")),
520
+ "mqtt_tls": bool(mqtt.get("tls") or _env_flag("MQTT_TLS") or False),
521
+ "mqtt_username": mqtt.get("username") or os.getenv("MQTT_USERNAME", ""),
522
+ "mqtt_password": mqtt.get("password") or os.getenv("MQTT_PASSWORD", ""),
523
+ "mqtt_client_id": mqtt.get("client_id")
524
+ or os.getenv("MQTT_CLIENT_ID", "voodoo-runtime"),
525
+ "mqtt_keepalive": int(mqtt.get("keepalive", 60)),
526
+ "mqtt_qos": int(mqtt.get("qos", 1)),
527
+ }
528
+ for key in (
529
+ "max_message_size",
530
+ "max_state_size",
531
+ "max_pending_effects",
532
+ "heartbeat_interval",
533
+ "http_host",
534
+ "http_port",
535
+ ):
536
+ if key in edge_data:
537
+ kwargs[key] = edge_data[key]
538
+ return EdgeConfig(**kwargs)
539
+
540
+
461
541
  def _pick(
462
542
  file_data: dict[str, Any],
463
543
  key: str,
@@ -551,6 +631,7 @@ def get_config(
551
631
  config_args["cache"] = _build_cache_config(file_data)
552
632
  config_args["models"] = _build_models_config(file_data)
553
633
  config_args["ai"] = _build_ai_config(file_data)
634
+ config_args["edge"] = _build_edge_config(file_data)
554
635
 
555
636
  # Existing sub-configurations
556
637
  if "seo" in file_data and isinstance(file_data["seo"], dict):
@@ -578,6 +659,7 @@ def get_config(
578
659
  "cache",
579
660
  "models",
580
661
  "ai",
662
+ "edge",
581
663
  "seo",
582
664
  "auth",
583
665
  "security",
@@ -241,6 +241,22 @@ def create_app(app_dir: str = "app") -> Starlette: # noqa: C901
241
241
 
242
242
  routes.extend(voodoo_api.routes)
243
243
 
244
+ # --- Edge device gateway (Sprint 23, EDGE §68) -------------------------
245
+ # Entirely optional: when ``edge.enabled`` is False, no store, gateway,
246
+ # routes, or MQTT client are created — regular apps pay zero overhead.
247
+ edge_gateway: list = [] # mutable holder so lifespan can share/replace the gateway
248
+ if config.edge.enabled and config.edge.http_enabled:
249
+ from voodoo.edge import DeviceGateway, SQLiteDeviceStore
250
+
251
+ edge_store = SQLiteDeviceStore(".voodoo/state/devices.db")
252
+ from voodoo.runtime.engine import engine as _runtime_engine
253
+
254
+ gateway = DeviceGateway(edge_store, _runtime_engine)
255
+ edge_gateway.append(gateway)
256
+ from voodoo.edge.http import build_edge_routes as _build_edge_routes
257
+
258
+ routes.extend(_build_edge_routes(gateway))
259
+
244
260
  from voodoo.auth import AuthMiddleware
245
261
  from voodoo.i18n import I18nMiddleware
246
262
  from voodoo.security import (
@@ -290,8 +306,39 @@ def create_app(app_dir: str = "app") -> Starlette: # noqa: C901
290
306
 
291
307
  if _workers:
292
308
  worker_task = asyncio.create_task(start_workers())
309
+
310
+ # Edge MQTT transport (Sprint 23) — optional, only when configured.
311
+ mqtt_transport = None
312
+ if config.edge.enabled and config.edge.mqtt_enabled:
313
+ try:
314
+ from voodoo.edge import DeviceGateway as _DG
315
+ from voodoo.edge import SQLiteDeviceStore as _SDS
316
+ from voodoo.edge.mqtt import EdgeMQTTTransport
317
+ from voodoo.runtime.engine import engine as _rt_engine
318
+
319
+ gateway = (
320
+ edge_gateway[0]
321
+ if edge_gateway
322
+ else _DG(_SDS(".voodoo/state/devices.db"), _rt_engine)
323
+ )
324
+ mqtt_transport = EdgeMQTTTransport(
325
+ gateway,
326
+ broker_url=config.edge.mqtt_broker_url or "localhost",
327
+ port=config.edge.mqtt_port,
328
+ tls=config.edge.mqtt_tls,
329
+ username=config.edge.mqtt_username or None,
330
+ password=config.edge.mqtt_password or None,
331
+ client_id=config.edge.mqtt_client_id,
332
+ qos=config.edge.mqtt_qos,
333
+ )
334
+ await mqtt_transport.start()
335
+ except ImportError:
336
+ # paho-mqtt not installed — edge stays HTTP-only.
337
+ pass
293
338
  yield
294
339
  # Shutdown
340
+ if mqtt_transport is not None:
341
+ await mqtt_transport.stop()
295
342
  await stop_workers()
296
343
  if worker_task:
297
344
  worker_task.cancel()
@@ -0,0 +1,122 @@
1
+ """Voodoo Edge — external device boundary for the Voodoo Runtime (Sprint 23).
2
+
3
+ Physical and distributed devices (ESP32, Raspberry Pi, robots, industrial
4
+ controllers) participate as first-class Voodoo Entities through the
5
+ versioned ``voodoo-edge/v1`` protocol over HTTP and MQTT transports.
6
+
7
+ There is **no** DeviceExecutionEngine: device-triggered work enters the
8
+ standard ExecutionEngine, device events use the standard event system,
9
+ and device state uses standard State semantics.
10
+
11
+ from voodoo.edge import DeviceGateway, SQLiteDeviceStore
12
+
13
+ store = SQLiteDeviceStore("data/devices.db")
14
+ gateway = DeviceGateway(store, engine)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from voodoo.edge.auth import (
20
+ authenticate_device,
21
+ consume_enrollment,
22
+ create_enrollment,
23
+ generate_device_credential,
24
+ hash_device_credential,
25
+ rotate_device_credential,
26
+ )
27
+ from voodoo.edge.errors import (
28
+ AuthenticationFailedError,
29
+ AuthorizationFailedError,
30
+ DeviceNotFoundError,
31
+ DeviceRevokedError,
32
+ DuplicateMessageError,
33
+ EdgeError,
34
+ EffectExpiredError,
35
+ EffectNotFoundError,
36
+ InvalidCapabilityError,
37
+ InvalidMessageError,
38
+ InvalidProtocolVersionError,
39
+ InvalidStateVersionError,
40
+ TransportError,
41
+ error_response,
42
+ )
43
+ from voodoo.edge.gateway import DeviceGateway
44
+ from voodoo.edge.models import (
45
+ AuthenticatedDeviceContext,
46
+ CredentialStatus,
47
+ Device,
48
+ DeviceCredential,
49
+ DeviceEnrollment,
50
+ DeviceSession,
51
+ DeviceStatus,
52
+ EnrollmentStatus,
53
+ TransportKind,
54
+ )
55
+ from voodoo.edge.protocol import (
56
+ PROTOCOL_NAME,
57
+ PROTOCOL_VERSION,
58
+ EdgeMessage,
59
+ EdgeMessageType,
60
+ EffectAckStatus,
61
+ decode_message,
62
+ encode_message,
63
+ make_message,
64
+ )
65
+ from voodoo.edge.store import (
66
+ DeviceStoreProtocol,
67
+ EffectDelivery,
68
+ InMemoryDeviceStore,
69
+ SQLiteDeviceStore,
70
+ )
71
+
72
+ __all__ = [
73
+ # Protocol identity
74
+ "PROTOCOL_NAME",
75
+ "PROTOCOL_VERSION",
76
+ # Models
77
+ "Device",
78
+ "DeviceStatus",
79
+ "DeviceCredential",
80
+ "CredentialStatus",
81
+ "DeviceEnrollment",
82
+ "EnrollmentStatus",
83
+ "DeviceSession",
84
+ "AuthenticatedDeviceContext",
85
+ "TransportKind",
86
+ # Protocol
87
+ "EdgeMessage",
88
+ "EdgeMessageType",
89
+ "EffectAckStatus",
90
+ "make_message",
91
+ "encode_message",
92
+ "decode_message",
93
+ # Store
94
+ "DeviceStoreProtocol",
95
+ "InMemoryDeviceStore",
96
+ "SQLiteDeviceStore",
97
+ "EffectDelivery",
98
+ # Auth
99
+ "authenticate_device",
100
+ "consume_enrollment",
101
+ "create_enrollment",
102
+ "generate_device_credential",
103
+ "hash_device_credential",
104
+ "rotate_device_credential",
105
+ # Gateway
106
+ "DeviceGateway",
107
+ # Errors
108
+ "EdgeError",
109
+ "error_response",
110
+ "AuthenticationFailedError",
111
+ "AuthorizationFailedError",
112
+ "DeviceNotFoundError",
113
+ "DeviceRevokedError",
114
+ "DuplicateMessageError",
115
+ "EffectExpiredError",
116
+ "EffectNotFoundError",
117
+ "InvalidCapabilityError",
118
+ "InvalidMessageError",
119
+ "InvalidProtocolVersionError",
120
+ "InvalidStateVersionError",
121
+ "TransportError",
122
+ ]
@@ -0,0 +1,243 @@
1
+ """Edge device authentication — credentials and enrollment (EDGE §9–§11).
2
+
3
+ Device authentication is deliberately separate from user authentication:
4
+ devices present dedicated ``vdk_`` credentials issued at enrollment,
5
+ never admin/user API keys. Only SHA-256 hashes are stored.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import secrets
12
+ from datetime import UTC, datetime, timedelta
13
+ from typing import TYPE_CHECKING
14
+
15
+ from voodoo.edge.errors import (
16
+ AuthenticationFailedError,
17
+ DeviceNotFoundError,
18
+ DeviceRevokedError,
19
+ )
20
+ from voodoo.edge.models import (
21
+ AuthenticatedDeviceContext,
22
+ Device,
23
+ DeviceCredential,
24
+ DeviceEnrollment,
25
+ DeviceSession,
26
+ DeviceStatus,
27
+ TransportKind,
28
+ _iso_now,
29
+ )
30
+ from voodoo.edge.store import DeviceStoreProtocol
31
+
32
+ if TYPE_CHECKING:
33
+ pass
34
+
35
+ __all__ = [
36
+ "generate_device_credential",
37
+ "hash_device_credential",
38
+ "generate_enrollment_key",
39
+ "hash_enrollment_key",
40
+ "authenticate_device",
41
+ "create_enrollment",
42
+ "consume_enrollment",
43
+ "rotate_device_credential",
44
+ ]
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Credential generation & hashing
49
+ # ---------------------------------------------------------------------------
50
+
51
+
52
+ def generate_device_credential() -> tuple[str, str]:
53
+ """Generate a device credential. Returns ``(raw_credential, hash)``.
54
+
55
+ The raw credential is shown exactly once — at enrollment or rotation.
56
+ Prefix ``vdk_`` (Voodoo Device Key) distinguishes device credentials
57
+ from user API keys (``vd_live_``).
58
+ """
59
+ raw = f"vdk_{secrets.token_urlsafe(32)}"
60
+ return raw, hash_device_credential(raw)
61
+
62
+
63
+ def hash_device_credential(credential: str) -> str:
64
+ """SHA-256 hash for safe storage."""
65
+ return hashlib.sha256(credential.encode("utf-8")).hexdigest()
66
+
67
+
68
+ def generate_enrollment_key() -> tuple[str, str]:
69
+ """Generate a single-use enrollment key. Returns ``(raw_key, hash)``."""
70
+ raw = f"vde_{secrets.token_urlsafe(24)}"
71
+ return raw, hash_device_credential(raw)
72
+
73
+
74
+ def hash_enrollment_key(key: str) -> str:
75
+ """SHA-256 hash of an enrollment key."""
76
+ return hashlib.sha256(key.encode("utf-8")).hexdigest()
77
+
78
+
79
+ def _parse_expires(expires_at: str | None) -> datetime | None:
80
+ if expires_at is None:
81
+ return None
82
+ try:
83
+ return datetime.fromisoformat(expires_at)
84
+ except ValueError:
85
+ return None
86
+
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # Enrollment (EDGE §10)
90
+ # ---------------------------------------------------------------------------
91
+
92
+
93
+ async def create_enrollment(
94
+ store: DeviceStoreProtocol,
95
+ *,
96
+ device_type: str = "generic",
97
+ device_name: str = "",
98
+ capabilities: list[str] | None = None,
99
+ expires_in_seconds: int = 3600,
100
+ ) -> str:
101
+ """Create a single-use, short-lived enrollment token.
102
+
103
+ Returns the **raw enrollment key** — store it securely; it cannot be
104
+ recovered from the runtime (only its hash is persisted).
105
+ """
106
+ raw_key, key_hash = generate_enrollment_key()
107
+ expires_at = (datetime.now(UTC) + timedelta(seconds=expires_in_seconds)).isoformat()
108
+ enrollment = DeviceEnrollment(
109
+ enrollment_key_hash=key_hash,
110
+ device_type=device_type,
111
+ device_name=device_name,
112
+ capabilities=list(capabilities or []),
113
+ expires_at=expires_at,
114
+ )
115
+ await store.create_enrollment(enrollment)
116
+ return raw_key
117
+
118
+
119
+ async def consume_enrollment(
120
+ store: DeviceStoreProtocol,
121
+ enrollment_key: str,
122
+ *,
123
+ firmware_version: str | None = None,
124
+ ) -> tuple[Device, str]:
125
+ """Consume an enrollment key and issue device identity + credential.
126
+
127
+ Validates: key exists, is PENDING, and not expired. On success the
128
+ enrollment becomes CONSUMED (single-use), a Device entity is created,
129
+ and a device credential is issued. Returns ``(device, raw_credential)``.
130
+ """
131
+ key_hash = hash_enrollment_key(enrollment_key)
132
+ enrollment = await store.find_enrollment_by_hash(key_hash)
133
+ if enrollment is None:
134
+ raise AuthenticationFailedError("Unknown enrollment key")
135
+
136
+ expires = _parse_expires(enrollment.expires_at)
137
+ if expires is not None and datetime.now(UTC) > expires:
138
+ raise AuthenticationFailedError("Enrollment key expired")
139
+
140
+ # Single-use: only a PENDING enrollment can convert.
141
+ consumed = await store.consume_enrollment(enrollment.enrollment_id)
142
+ if not consumed:
143
+ raise AuthenticationFailedError("Enrollment key already used or revoked")
144
+
145
+ device = Device(
146
+ type=enrollment.device_type,
147
+ name=enrollment.device_name or f"{enrollment.device_type}-device",
148
+ capabilities=list(enrollment.capabilities),
149
+ metadata={"firmware_version": firmware_version} if firmware_version else {},
150
+ )
151
+ await store.register_device(device)
152
+
153
+ raw_credential, credential_hash = generate_device_credential()
154
+ await store.create_credential(
155
+ DeviceCredential(
156
+ device_id=device.device_id,
157
+ credential_hash=credential_hash,
158
+ )
159
+ )
160
+ return device, raw_credential
161
+
162
+
163
+ async def rotate_device_credential(store: DeviceStoreProtocol, device_id: str) -> str:
164
+ """Rotate a device credential. Returns the new raw credential.
165
+
166
+ The old credential is marked ROTATED (no longer valid); the device
167
+ keeps operating with the new one.
168
+ """
169
+ old = await store.get_active_credential(device_id)
170
+ if old is not None:
171
+ await store.revoke_credential(old.credential_id)
172
+
173
+ raw, credential_hash = generate_device_credential()
174
+ await store.create_credential(
175
+ DeviceCredential(
176
+ device_id=device_id,
177
+ credential_hash=credential_hash,
178
+ )
179
+ )
180
+ return raw
181
+
182
+
183
+ # ---------------------------------------------------------------------------
184
+ # Authentication (EDGE §11)
185
+ # ---------------------------------------------------------------------------
186
+
187
+
188
+ async def authenticate_device(
189
+ store: DeviceStoreProtocol,
190
+ credential: str,
191
+ *,
192
+ claimed_device_id: str | None = None,
193
+ transport: TransportKind = TransportKind.HTTP,
194
+ ) -> tuple[AuthenticatedDeviceContext, DeviceSession]:
195
+ """Authenticate a device credential and establish a session.
196
+
197
+ The credential MUST bind to the claimed device — a message claiming
198
+ ``device_id`` without a matching credential is rejected (EDGE §11).
199
+
200
+ Returns ``(context, session)``. Raises:
201
+ - ``AuthenticationFailedError`` — unknown/invalid credential
202
+ - ``DeviceRevokedError`` — device has been revoked
203
+ - ``DeviceNotFoundError`` — claimed device does not exist
204
+ """
205
+ credential_hash = hash_device_credential(credential)
206
+ record = await store.find_credential_by_hash(credential_hash)
207
+ if record is None:
208
+ raise AuthenticationFailedError("Invalid device credential")
209
+
210
+ # Never trust a claimed device_id without validating the binding.
211
+ if claimed_device_id is not None and claimed_device_id != record.device_id:
212
+ raise AuthenticationFailedError(
213
+ "Credential does not belong to the claimed device"
214
+ )
215
+
216
+ device = await store.get_device(record.device_id)
217
+ if device is None:
218
+ raise DeviceNotFoundError(
219
+ f"Credential references missing device '{record.device_id}'"
220
+ )
221
+ if device.status == DeviceStatus.REVOKED:
222
+ raise DeviceRevokedError(f"Device '{device.device_id}' has been revoked")
223
+
224
+ await store.mark_credential_used(record.credential_id)
225
+
226
+ session = DeviceSession(
227
+ device_id=device.device_id,
228
+ transport=transport,
229
+ last_seen_at=_iso_now(),
230
+ )
231
+ await store.create_session(session)
232
+ await store.update_device_status(device.device_id, DeviceStatus.CONNECTED)
233
+ await store.update_last_seen(device.device_id)
234
+
235
+ context = AuthenticatedDeviceContext(
236
+ device_id=device.device_id,
237
+ device_type=device.type,
238
+ capabilities=list(device.capabilities),
239
+ session_id=session.session_id,
240
+ transport=transport,
241
+ metadata=dict(device.metadata),
242
+ )
243
+ return context, session