voodoo-framework 2.1.0__tar.gz → 2.3.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 (224) hide show
  1. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/PKG-INFO +5 -1
  2. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/pyproject.toml +5 -0
  3. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/__init__.py +13 -1
  4. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/__init__.py +4 -0
  5. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/doctor.py +48 -0
  6. voodoo_framework-2.3.0/src/voodoo/cli/status.py +73 -0
  7. voodoo_framework-2.3.0/src/voodoo/cli/workers_cmd.py +71 -0
  8. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/primitives/effect.py +9 -0
  9. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/runtime/capability.py +54 -4
  10. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/runtime/engine.py +23 -4
  11. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/security/__init__.py +26 -0
  12. voodoo_framework-2.3.0/src/voodoo/security/redaction.py +175 -0
  13. voodoo_framework-2.3.0/src/voodoo/security/secrets.py +223 -0
  14. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/telemetry/__init__.py +4 -0
  15. voodoo_framework-2.3.0/src/voodoo/telemetry/otlp.py +108 -0
  16. voodoo_framework-2.3.0/src/voodoo/telemetry/store.py +355 -0
  17. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/workers/queue.py +9 -2
  18. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo_framework.egg-info/PKG-INFO +5 -1
  19. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo_framework.egg-info/SOURCES.txt +7 -0
  20. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo_framework.egg-info/requires.txt +5 -0
  21. voodoo_framework-2.3.0/tests/test_capability_security.py +426 -0
  22. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_contract_api.py +11 -0
  23. voodoo_framework-2.3.0/tests/test_observability.py +459 -0
  24. voodoo_framework-2.1.0/src/voodoo/telemetry/store.py +0 -200
  25. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/LICENSE +0 -0
  26. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/README.md +0 -0
  27. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/setup.cfg +0 -0
  28. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/adapters/__init__.py +0 -0
  29. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/adapters/capabilities.py +0 -0
  30. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/adapters/registry.py +0 -0
  31. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/adapters/tailwind.py +0 -0
  32. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/adapters/voodoo_css.py +0 -0
  33. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/agent.py +0 -0
  34. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/agents/__init__.py +0 -0
  35. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/agents/models.py +0 -0
  36. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/agents/registry.py +0 -0
  37. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/ai/__init__.py +0 -0
  38. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/ai/agent.py +0 -0
  39. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/ai/providers/__init__.py +0 -0
  40. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/ai/providers/anthropic.py +0 -0
  41. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/ai/providers/gemini.py +0 -0
  42. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/ai/providers/mock.py +0 -0
  43. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/ai/providers/ollama.py +0 -0
  44. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/ai/providers/openai.py +0 -0
  45. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/ai/tools/__init__.py +0 -0
  46. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/ai/tools/registry.py +0 -0
  47. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/api.py +0 -0
  48. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/auth/__init__.py +0 -0
  49. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/auth/cookies.py +0 -0
  50. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/auth/guards.py +0 -0
  51. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/auth/jwt.py +0 -0
  52. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/auth/middleware.py +0 -0
  53. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/auth/passwords.py +0 -0
  54. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/auth/user.py +0 -0
  55. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/__main__.py +0 -0
  56. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/agents.py +0 -0
  57. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/ai.py +0 -0
  58. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/approvals.py +0 -0
  59. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/auth.py +0 -0
  60. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/dev.py +0 -0
  61. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/executions.py +0 -0
  62. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/generate.py +0 -0
  63. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/inspect.py +0 -0
  64. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/new.py +0 -0
  65. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/objects.py +0 -0
  66. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/recover.py +0 -0
  67. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/routes.py +0 -0
  68. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/scaffolding.py +0 -0
  69. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/schedules.py +0 -0
  70. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/tasks.py +0 -0
  71. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/terminal.py +0 -0
  72. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/theme.py +0 -0
  73. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/cli/version.py +0 -0
  74. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/components.py +0 -0
  75. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/config.py +0 -0
  76. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/core/__init__.py +0 -0
  77. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/core/app.py +0 -0
  78. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/core/errors.py +0 -0
  79. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/core/events.py +0 -0
  80. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/core/render.py +0 -0
  81. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/core/routing.py +0 -0
  82. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/core/sitemap.py +0 -0
  83. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/core/state.py +0 -0
  84. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/data/__init__.py +0 -0
  85. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/data/base.py +0 -0
  86. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/data/model.py +0 -0
  87. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/i18n.py +0 -0
  88. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/mcp/__init__.py +0 -0
  89. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/mcp/client.py +0 -0
  90. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/memory/__init__.py +0 -0
  91. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/memory/interfaces.py +0 -0
  92. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/memory/sqlite.py +0 -0
  93. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/mesh/__init__.py +0 -0
  94. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/mesh/client.py +0 -0
  95. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/mesh/event_bus.py +0 -0
  96. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/primitives/__init__.py +0 -0
  97. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/primitives/capability.py +0 -0
  98. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/primitives/compute.py +0 -0
  99. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/primitives/constraint.py +0 -0
  100. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/primitives/intent.py +0 -0
  101. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/primitives/resource.py +0 -0
  102. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/primitives/state.py +0 -0
  103. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/primitives/time.py +0 -0
  104. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/py.typed +0 -0
  105. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/queue.py +0 -0
  106. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/routing/api.py +0 -0
  107. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/routing/pages.py +0 -0
  108. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/runtime/__init__.py +0 -0
  109. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/runtime/adaptive.py +0 -0
  110. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/runtime/constraint.py +0 -0
  111. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/runtime/context.py +0 -0
  112. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/runtime/errors.py +0 -0
  113. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/runtime/execution.py +0 -0
  114. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/runtime/graph.py +0 -0
  115. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/runtime/human.py +0 -0
  116. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/runtime/persistence.py +0 -0
  117. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/runtime/planner.py +0 -0
  118. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/runtime/scheduler.py +0 -0
  119. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/runtime/task.py +0 -0
  120. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/runtime/workflow.py +0 -0
  121. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/schedule.py +0 -0
  122. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/security/cors.py +0 -0
  123. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/security/csrf.py +0 -0
  124. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/security/headers.py +0 -0
  125. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/security/passwords.py +0 -0
  126. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/security/rate_limit.py +0 -0
  127. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/seo.py +0 -0
  128. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/static/client.js +0 -0
  129. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/status.py +0 -0
  130. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/__init__.py +0 -0
  131. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/cache/__init__.py +0 -0
  132. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/cache/interfaces.py +0 -0
  133. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/cache/memory.py +0 -0
  134. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/cache/redis.py +0 -0
  135. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/database/__init__.py +0 -0
  136. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/database/interfaces.py +0 -0
  137. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/database/postgres.py +0 -0
  138. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/database/sqlite.py +0 -0
  139. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/events/__init__.py +0 -0
  140. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/events/interfaces.py +0 -0
  141. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/events/local.py +0 -0
  142. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/events/postgres.py +0 -0
  143. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/events/sqlite.py +0 -0
  144. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/execution/__init__.py +0 -0
  145. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/execution/migrations.py +0 -0
  146. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/execution/postgres.py +0 -0
  147. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/execution/sqlite.py +0 -0
  148. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/manager.py +0 -0
  149. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/objects/__init__.py +0 -0
  150. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/objects/interfaces.py +0 -0
  151. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/objects/local.py +0 -0
  152. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/objects/s3.py +0 -0
  153. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/queue/__init__.py +0 -0
  154. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/queue/interfaces.py +0 -0
  155. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/queue/memory.py +0 -0
  156. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/queue/postgres.py +0 -0
  157. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/queue/redis.py +0 -0
  158. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/queue/sqlite.py +0 -0
  159. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/scheduler/__init__.py +0 -0
  160. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/storage/scheduler/sqlite.py +0 -0
  161. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/telemetry/api.py +0 -0
  162. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/telemetry/middleware.py +0 -0
  163. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/theme.py +0 -0
  164. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/tools/__init__.py +0 -0
  165. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/tools/registry.py +0 -0
  166. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/ui/__init__.py +0 -0
  167. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/ui/component.py +0 -0
  168. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/ui/events.py +0 -0
  169. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/ui/library.py +0 -0
  170. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/ui/markdown.py +0 -0
  171. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/ui/rendering.py +0 -0
  172. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/ui/state.py +0 -0
  173. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/ui/styles/__init__.py +0 -0
  174. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/ui/styles/presets.py +0 -0
  175. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/ui/styles/theme.py +0 -0
  176. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo/workers/__init__.py +0 -0
  177. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo_framework.egg-info/dependency_links.txt +0 -0
  178. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo_framework.egg-info/entry_points.txt +0 -0
  179. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/src/voodoo_framework.egg-info/top_level.txt +0 -0
  180. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_agent.py +0 -0
  181. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_agent_registry.py +0 -0
  182. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_agent_runtime.py +0 -0
  183. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_app.py +0 -0
  184. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_auth.py +0 -0
  185. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_checkpoint_resume.py +0 -0
  186. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_chrome.py +0 -0
  187. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_cli.py +0 -0
  188. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_cli_inspect.py +0 -0
  189. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_components.py +0 -0
  190. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_config.py +0 -0
  191. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_data.py +0 -0
  192. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_design_system.py +0 -0
  193. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_durable_hitl.py +0 -0
  194. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_events.py +0 -0
  195. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_execution_sqlite_store.py +0 -0
  196. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_http_runtime.py +0 -0
  197. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_human.py +0 -0
  198. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_i18n.py +0 -0
  199. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_integration.py +0 -0
  200. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_mcp.py +0 -0
  201. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_mcp_runtime.py +0 -0
  202. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_memory.py +0 -0
  203. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_mesh.py +0 -0
  204. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_mesh_event_bus.py +0 -0
  205. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_model.py +0 -0
  206. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_object_store.py +0 -0
  207. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_persistence.py +0 -0
  208. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_planner_adaptive.py +0 -0
  209. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_primitives.py +0 -0
  210. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_provider_switching.py +0 -0
  211. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_providers.py +0 -0
  212. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_queue.py +0 -0
  213. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_reactive.py +0 -0
  214. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_runtime.py +0 -0
  215. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_scheduler.py +0 -0
  216. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_security.py +0 -0
  217. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_seo.py +0 -0
  218. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_state.py +0 -0
  219. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_theme.py +0 -0
  220. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_theme_presets.py +0 -0
  221. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_tools.py +0 -0
  222. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_ui.py +0 -0
  223. {voodoo_framework-2.1.0 → voodoo_framework-2.3.0}/tests/test_websocket.py +0 -0
  224. {voodoo_framework-2.1.0 → voodoo_framework-2.3.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.1.0
3
+ Version: 2.3.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,10 @@ 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: otel
63
+ Requires-Dist: opentelemetry-api>=1.20; extra == "otel"
64
+ Requires-Dist: opentelemetry-sdk>=1.20; extra == "otel"
65
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.20; extra == "otel"
62
66
  Dynamic: license-file
63
67
 
64
68
  <picture>
@@ -93,6 +93,11 @@ s3 = [
93
93
  redis = [
94
94
  "redis>=5.0",
95
95
  ]
96
+ otel = [
97
+ "opentelemetry-api>=1.20",
98
+ "opentelemetry-sdk>=1.20",
99
+ "opentelemetry-exporter-otlp-proto-grpc>=1.20",
100
+ ]
96
101
 
97
102
  [tool.setuptools.dynamic]
98
103
  version = { attr = "voodoo.__version__" }
@@ -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.1.0"
105
+ __version__ = "2.3.0"
106
106
 
107
107
  __all__ = [
108
108
  # Core runtime
@@ -306,6 +306,18 @@ _DEPRECATED_EXPORTS: dict[str, str] = {
306
306
  "rate_limiter": "voodoo.security",
307
307
  "set_csrf_cookie": "voodoo.security",
308
308
  "validate_password_strength": "voodoo.security",
309
+ # voodoo.security — Sprint 19
310
+ "SecretStore": "voodoo.security",
311
+ "EnvSecretStore": "voodoo.security",
312
+ "LocalSecretStore": "voodoo.security",
313
+ "SecretsError": "voodoo.security",
314
+ "secrets": "voodoo.security",
315
+ "configure_secrets": "voodoo.security",
316
+ "RedactionGuard": "voodoo.security",
317
+ "redact": "voodoo.security",
318
+ "redact_string": "voodoo.security",
319
+ # voodoo.runtime — Sprint 19
320
+ "SENSITIVE_CAPABILITIES": "voodoo.runtime.capability",
309
321
  # voodoo.seo
310
322
  "FAQ": "voodoo.seo",
311
323
  "GEO": "voodoo.seo",
@@ -13,9 +13,11 @@ from voodoo.cli import (
13
13
  recover,
14
14
  routes,
15
15
  schedules,
16
+ status,
16
17
  tasks,
17
18
  theme,
18
19
  version,
20
+ workers_cmd,
19
21
  )
20
22
  from voodoo.cli.auth import auth_app
21
23
  from voodoo.cli.inspect import inspect_app
@@ -45,6 +47,8 @@ app.add_typer(theme.theme_app, name="theme")
45
47
  # (timeline from the journal) and `voodoo events` (recent journal events).
46
48
  app.command("execution")(executions.show_execution)
47
49
  app.command("events")(executions.list_events)
50
+ app.command()(status.status)
51
+ app.command()(workers_cmd.workers)
48
52
  app.command()(version.version)
49
53
  app.command()(doctor.doctor)
50
54
  app.command()(routes.routes)
@@ -217,6 +217,54 @@ def doctor(): # noqa: C901
217
217
  except Exception: # noqa: BLE001 - diagnostics must never crash doctor
218
218
  terminal.status("capability matrix", "unavailable")
219
219
 
220
+ # ── Queue depth ─────────────────────────────────
221
+ terminal.heading("queue")
222
+ try:
223
+ import asyncio
224
+
225
+ async def _check_queue() -> tuple[int, int]:
226
+ from voodoo.workers.queue import _get_queue, _workers
227
+
228
+ depth = 0
229
+ try:
230
+ q = await _get_queue()
231
+ if hasattr(q, "list"):
232
+ tasks = await q.list()
233
+ depth = len(tasks)
234
+ elif hasattr(q, "depth"):
235
+ depth = await q.depth()
236
+ except Exception:
237
+ pass
238
+ return depth, len(_workers)
239
+
240
+ depth, n_workers = asyncio.run(_check_queue())
241
+ terminal.status("depth", str(depth))
242
+ terminal.status("registered workers", str(n_workers))
243
+ except Exception: # noqa: BLE001
244
+ terminal.status("queue", "unavailable")
245
+
246
+ # ── Schedules ───────────────────────────────────
247
+ terminal.heading("schedules")
248
+ try:
249
+ from pathlib import Path as _Path
250
+
251
+ sched_db = _Path(cfg.db_path).parent / "schedules.db"
252
+ if sched_db.exists():
253
+ terminal.status("scheduler db", "present")
254
+ else:
255
+ terminal.status("scheduler db", "not found")
256
+ except Exception: # noqa: BLE001
257
+ terminal.status("schedules", "unavailable")
258
+
259
+ # ── OTLP ────────────────────────────────────────
260
+ terminal.heading("otel")
261
+ try:
262
+ from voodoo.telemetry.otlp import is_available
263
+
264
+ terminal.status("otlp exporter", "active" if is_available() else "off")
265
+ except Exception: # noqa: BLE001
266
+ terminal.status("otlp exporter", "off")
267
+
220
268
  # ── AI Kit ──────────────────────────────────────
221
269
  terminal.heading("ai kit")
222
270
 
@@ -0,0 +1,73 @@
1
+ """``voodoo status`` — runtime health overview.
2
+
3
+ Displays a quick snapshot of the runtime: request counts, error rate,
4
+ latency, agent activity, queue depth, and OTel export status.
5
+ Works after a restart because rolling counters are persisted to
6
+ ``.voodoo/telemetry_summary.json``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import typer
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+
15
+ __all__ = ["status"]
16
+
17
+
18
+ def status() -> None:
19
+ """Show runtime health overview."""
20
+ from voodoo.telemetry import telemetry_store
21
+ from voodoo.telemetry.otlp import is_available as otlp_available
22
+
23
+ summary = telemetry_store.get_summary()
24
+ console = Console()
25
+
26
+ # -- header --------------------------------------------------------
27
+ console.print("[bold cyan]Voodoo Runtime Status[/bold cyan]")
28
+ console.print()
29
+
30
+ # -- requests & errors ---------------------------------------------
31
+ tbl = Table(show_header=True, header_style="bold")
32
+ tbl.add_column("Metric", style="dim")
33
+ tbl.add_column("Value", justify="right")
34
+
35
+ total = summary.get("requests_total", 0)
36
+ errors = summary.get("errors_total", 0)
37
+ error_rate = f"{(errors / total * 100):.1f}%" if total else "—"
38
+ avg_lat = summary.get("average_latency_ms", 0.0)
39
+
40
+ tbl.add_row("Requests total", str(total))
41
+ tbl.add_row("Errors total", str(errors))
42
+ tbl.add_row("Error rate", error_rate)
43
+ tbl.add_row("Avg latency", f"{avg_lat:.1f} ms")
44
+ tbl.add_row("DB queries", str(summary.get("db_queries", 0)))
45
+ console.print(tbl)
46
+ console.print()
47
+
48
+ # -- agent activity ------------------------------------------------
49
+ agent_tbl = Table(show_header=True, header_style="bold")
50
+ agent_tbl.add_column("Agent Metric", style="dim")
51
+ agent_tbl.add_column("Value", justify="right")
52
+
53
+ agent_tbl.add_row("Agent runs", str(summary.get("agent_runs", 0)))
54
+ agent_tbl.add_row("Tokens in", str(summary.get("agent_tokens_in", 0)))
55
+ agent_tbl.add_row("Tokens out", str(summary.get("agent_tokens_out", 0)))
56
+ agent_tbl.add_row("Agent cost", f"${summary.get('agent_cost', 0.0):.6f}")
57
+ agent_tbl.add_row("Tool calls", str(summary.get("tool_calls_total", 0)))
58
+ agent_tbl.add_row("Tool errors", str(summary.get("tool_errors", 0)))
59
+ agent_tbl.add_row("Spans recorded", str(summary.get("spans_total", 0)))
60
+ console.print(agent_tbl)
61
+ console.print()
62
+
63
+ # -- infrastructure ------------------------------------------------
64
+ infra_tbl = Table(show_header=True, header_style="bold")
65
+ infra_tbl.add_column("Infrastructure", style="dim")
66
+ infra_tbl.add_column("Status")
67
+
68
+ infra_tbl.add_row("OTLP export", "active" if otlp_available() else "off")
69
+ console.print(infra_tbl)
70
+
71
+
72
+ app = typer.Typer(name="status", help="Runtime health overview.", no_args_is_help=True)
73
+ app.command()(status)
@@ -0,0 +1,71 @@
1
+ """``voodoo workers`` — worker state & queue depth.
2
+
3
+ Shows registered background workers, their queue depth, and whether
4
+ the worker loop is running.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ from typing import Any
11
+
12
+ import typer
13
+ from rich.console import Console
14
+ from rich.table import Table
15
+
16
+ __all__ = ["workers"]
17
+
18
+
19
+ async def _gather_worker_info() -> dict[str, Any]:
20
+ """Collect worker and queue state without side-effects."""
21
+ from voodoo.workers.queue import _get_queue, _worker_tasks, _workers
22
+
23
+ info: dict[str, Any] = {
24
+ "registered": list(_workers.keys()),
25
+ "running_tasks": len([t for t in _worker_tasks if not t.done()]),
26
+ "queue_depth": 0,
27
+ }
28
+
29
+ try:
30
+ q = await _get_queue()
31
+ if hasattr(q, "list"):
32
+ tasks = await q.list()
33
+ info["queue_depth"] = len(tasks)
34
+ elif hasattr(q, "depth"):
35
+ info["queue_depth"] = await q.depth()
36
+ except Exception:
37
+ pass
38
+
39
+ return info
40
+
41
+
42
+ def workers() -> None:
43
+ """Show registered workers and queue depth."""
44
+ console = Console()
45
+ info = asyncio.run(_gather_worker_info())
46
+
47
+ console.print("[bold cyan]Voodoo Workers[/bold cyan]")
48
+ console.print()
49
+
50
+ tbl = Table(show_header=True, header_style="bold")
51
+ tbl.add_column("Worker", style="dim")
52
+ tbl.add_column("Status")
53
+
54
+ registered = info["registered"]
55
+ if not registered:
56
+ console.print("[dim]No workers registered.[/dim]")
57
+ else:
58
+ running = info["running_tasks"]
59
+ for name in registered:
60
+ tbl.add_row(name, "running" if running > 0 else "idle")
61
+ console.print(tbl)
62
+
63
+ console.print()
64
+ console.print(f"Queue depth: [bold]{info['queue_depth']}[/bold]")
65
+ console.print(f"Running worker tasks: [bold]{info['running_tasks']}[/bold]")
66
+
67
+
68
+ app = typer.Typer(
69
+ name="workers", help="Worker state & queue depth.", no_args_is_help=True
70
+ )
71
+ app.command()(workers)
@@ -61,6 +61,11 @@ class Effect(BaseModel):
61
61
  error: str | None = None
62
62
  created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
63
63
  executed_at: datetime | None = None
64
+ # Authorization context (Sprint 19, ROADMAP §55)
65
+ actor: str | None = None
66
+ principal: str | None = None
67
+ resource: str | None = None
68
+ scope: str | None = None
64
69
 
65
70
  # -- lifecycle ---------------------------------------------------------
66
71
 
@@ -114,6 +119,10 @@ class Effect(BaseModel):
114
119
  "status": self.status.value,
115
120
  "intent_id": self.intent_id,
116
121
  "capability": self.capability_name,
122
+ "actor": self.actor,
123
+ "principal": self.principal,
124
+ "resource": self.resource,
125
+ "scope": self.scope,
117
126
  "reversible": self.reversible,
118
127
  "idempotent": self.idempotent,
119
128
  "idempotency_key": self.idempotency_key,
@@ -23,7 +23,23 @@ from voodoo.primitives.capability import Capability
23
23
  from voodoo.runtime.context import ExecutionContext
24
24
  from voodoo.runtime.errors import ApprovalRequired, CapabilityDenied
25
25
 
26
- __all__ = ["Resolution", "CapabilityResolver"]
26
+ __all__ = ["Resolution", "CapabilityResolver", "SENSITIVE_CAPABILITIES"]
27
+
28
+
29
+ #: Capabilities that require explicit grants — no ambient authority by
30
+ #: default (Sprint 19, ROADMAP §55). An agent or tool that attempts to
31
+ #: use one of these without an explicit grant gets a :class:`CapabilityDenied`
32
+ #: error, never a silent pass-through.
33
+ SENSITIVE_CAPABILITIES: frozenset[str] = frozenset(
34
+ {
35
+ "filesystem.write",
36
+ "network.request",
37
+ "shell.execute",
38
+ "secrets.read",
39
+ "payment.execute",
40
+ "email.send",
41
+ }
42
+ )
27
43
 
28
44
 
29
45
  class Resolution(StrEnum):
@@ -65,17 +81,50 @@ class CapabilityResolver:
65
81
  Context-granted capabilities take precedence (they are the
66
82
  delegated authority for an in-flight execution). Otherwise the
67
83
  registry is consulted.
84
+
85
+ Sensitive capabilities (Sprint 19) are denied by default — they
86
+ require an explicit grant in the context or registry.
68
87
  """
88
+ if name in SENSITIVE_CAPABILITIES:
89
+ return self._resolve_sensitive(name, scope=scope, context=context)
90
+ return self._resolve_standard(name, scope=scope, context=context)
91
+
92
+ def _resolve_sensitive(
93
+ self,
94
+ name: str,
95
+ *,
96
+ scope: str | None = None,
97
+ context: ExecutionContext | None = None,
98
+ ) -> Resolution:
99
+ """Resolve a sensitive capability — requires explicit grant."""
69
100
  if context is not None and context.has_capability(name, scope=scope):
70
- if name in self.approval_capabilities:
71
- return Resolution.REQUIRES_APPROVAL
72
- return Resolution.ALLOWED
101
+ return self._check_approval(name)
102
+ cap = self.capabilities.get(name)
103
+ if cap is not None and cap.valid:
104
+ if scope is not None and cap.scope is not None and cap.scope != scope:
105
+ return Resolution.DENIED
106
+ return self._check_approval(name)
107
+ return Resolution.DENIED
73
108
 
109
+ def _resolve_standard(
110
+ self,
111
+ name: str,
112
+ *,
113
+ scope: str | None = None,
114
+ context: ExecutionContext | None = None,
115
+ ) -> Resolution:
116
+ """Resolve a standard capability — normal registry rules."""
117
+ if context is not None and context.has_capability(name, scope=scope):
118
+ return self._check_approval(name)
74
119
  cap = self.capabilities.get(name)
75
120
  if cap is None or not cap.valid:
76
121
  return Resolution.DENIED
77
122
  if scope is not None and cap.scope is not None and cap.scope != scope:
78
123
  return Resolution.DENIED
124
+ return self._check_approval(name)
125
+
126
+ def _check_approval(self, name: str) -> Resolution:
127
+ """Check whether a capability requires human approval."""
79
128
  if name in self.approval_capabilities:
80
129
  return Resolution.REQUIRES_APPROVAL
81
130
  return Resolution.ALLOWED
@@ -114,4 +163,5 @@ class CapabilityResolver:
114
163
  return {
115
164
  "capabilities": [c.name for c in self.capabilities.values()],
116
165
  "approval_required": sorted(self.approval_capabilities),
166
+ "sensitive": sorted(SENSITIVE_CAPABILITIES),
117
167
  }
@@ -173,10 +173,16 @@ class ExecutionEngine:
173
173
  def _journal_approval_decision(
174
174
  self, execution_id: str, event: str, payload: dict
175
175
  ) -> None:
176
- """Record an approval decision as a journal event (Sprint 4)."""
176
+ """Record an approval decision as a journal event (Sprint 4).
177
+
178
+ Applies redaction to prevent secrets from leaking into the
179
+ execution journal (Sprint 19, ROADMAP §55).
180
+ """
177
181
  store = self._execution_store
178
182
  if store is not None and hasattr(store, "append_event"):
179
- store.append_event(execution_id, event, payload)
183
+ from voodoo.security.redaction import redact
184
+
185
+ store.append_event(execution_id, event, redact(payload))
180
186
 
181
187
  def checkpoint(self, execution: Execution) -> None:
182
188
  """Public checkpoint API — persists an execution mid-flight.
@@ -511,6 +517,13 @@ class ExecutionEngine:
511
517
  """Record a compute result's effects, state changes and resources."""
512
518
  for effect in result.effects:
513
519
  effect.intent_id = intent.id
520
+ # Authorization context (Sprint 19, ROADMAP §55): record
521
+ # who triggered the effect, which capability authorized it,
522
+ # and what resource/scope it targets.
523
+ if effect.actor is None:
524
+ effect.actor = ctx.actor
525
+ if effect.capability_name is None and ctx.capabilities:
526
+ effect.capability_name = ctx.capabilities[0].name
514
527
  # Idempotency key: stable per execution+effect so a resumed
515
528
  # execution can safely skip already-completed effects (spec §15).
516
529
  if effect.idempotency_key is None:
@@ -722,15 +735,21 @@ class ExecutionEngine:
722
735
  return out
723
736
 
724
737
  async def _emit(self, event: str, payload: dict[str, Any]) -> None:
725
- """Publish a namespaced mesh event (best-effort, never breaks)."""
738
+ """Publish a namespaced mesh event (best-effort, never breaks).
739
+
740
+ Applies redaction to prevent secrets from leaking into the
741
+ event bus (Sprint 19, ROADMAP §55).
742
+ """
726
743
  try:
727
744
  from voodoo.mesh import mesh
745
+ from voodoo.security.redaction import redact
728
746
 
729
- await mesh.broadcast(event, payload)
747
+ await mesh.broadcast(event, redact(payload))
730
748
  except Exception: # noqa: BLE001
731
749
  pass
732
750
 
733
751
  def _record_telemetry(self, execution: Execution) -> None:
752
+ """Record execution telemetry with redaction (Sprint 19)."""
734
753
  try:
735
754
  from voodoo.telemetry import telemetry_store
736
755
 
@@ -10,6 +10,10 @@ This package re-exports the names that previously lived in the flat
10
10
  - ``voodoo/security/rate_limit.py`` — ``RateLimiter``, ``RateLimitMiddleware``
11
11
  and the shared ``rate_limiter`` singleton
12
12
  - ``voodoo/security/passwords.py`` — ``validate_password_strength``
13
+ - ``voodoo/security/secrets.py`` — ``SecretStore``, ``EnvSecretStore``,
14
+ ``LocalSecretStore``, ``secrets``, ``configure`` (Sprint 19)
15
+ - ``voodoo/security/redaction.py`` — ``redact``, ``redact_string``,
16
+ ``RedactionGuard`` (Sprint 19)
13
17
  """
14
18
 
15
19
  from voodoo.security.cors import CORSMiddleware
@@ -25,6 +29,17 @@ from voodoo.security.rate_limit import (
25
29
  RateLimitMiddleware,
26
30
  rate_limiter,
27
31
  )
32
+ from voodoo.security.redaction import RedactionGuard, redact, redact_string
33
+ from voodoo.security.secrets import (
34
+ EnvSecretStore,
35
+ LocalSecretStore,
36
+ SecretsError,
37
+ SecretStore,
38
+ secrets,
39
+ )
40
+ from voodoo.security.secrets import (
41
+ configure as configure_secrets,
42
+ )
28
43
 
29
44
  __all__ = [
30
45
  # Headers
@@ -41,4 +56,15 @@ __all__ = [
41
56
  "rate_limiter",
42
57
  # Passwords
43
58
  "validate_password_strength",
59
+ # Secrets (Sprint 19)
60
+ "SecretStore",
61
+ "EnvSecretStore",
62
+ "LocalSecretStore",
63
+ "SecretsError",
64
+ "secrets",
65
+ "configure_secrets",
66
+ # Redaction (Sprint 19)
67
+ "RedactionGuard",
68
+ "redact",
69
+ "redact_string",
44
70
  ]
@@ -0,0 +1,175 @@
1
+ """Redaction guard — prevent secrets from leaking into observability (Sprint 19).
2
+
3
+ Centralized redaction for all output channels: events, journal entries,
4
+ telemetry records, mesh payloads, and log output.
5
+
6
+ The guard scrubs:
7
+
8
+ 1. Values of registered secrets (from :mod:`voodoo.security.secrets`).
9
+ 2. Common secret patterns (API keys, bearer tokens, password URIs).
10
+ 3. Keys named ``secret``, ``password``, ``token``, ``api_key``, etc.
11
+
12
+ Usage::
13
+
14
+ from voodoo.security.redaction import redact
15
+
16
+ safe = redact({"api_key": "sk-abc123", "name": "test"})
17
+ # → {"api_key": "[REDACTED]", "name": "test"}
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import re
23
+ from typing import Any
24
+
25
+ __all__ = [
26
+ "redact",
27
+ "redact_string",
28
+ "RedactionGuard",
29
+ ]
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Patterns
33
+ # ---------------------------------------------------------------------------
34
+
35
+ #: Keys whose values are always redacted (case-insensitive partial match).
36
+ _SENSITIVE_KEY_PATTERNS: re.Pattern[str] = re.compile(
37
+ r"(?i)(secret|password|passwd|token|api[_-]?key|auth|credential|private[_-]?key|"
38
+ r"access[_-]?key|session[_-]?id|cookie)",
39
+ )
40
+
41
+ #: Value patterns that look like secrets regardless of key name.
42
+ _SENSITIVE_VALUE_PATTERNS: list[re.Pattern[str]] = [
43
+ # Bearer tokens
44
+ re.compile(r"(?i)bearer\s+[A-Za-z0-9\-._~+/]+=*"),
45
+ # OpenAI-style keys
46
+ re.compile(r"sk-[A-Za-z0-9]{20,}"),
47
+ # Anthropic-style keys
48
+ re.compile(r"sk-ant-[A-Za-z0-9\-]{20,}"),
49
+ # AWS access keys
50
+ re.compile(r"AKIA[0-9A-Z]{16}"),
51
+ # Generic long hex tokens (32+ chars)
52
+ re.compile(r"\b[A-Fa-f0-9]{32,}\b"),
53
+ # JWT tokens (three base64 segments)
54
+ re.compile(r"eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+"),
55
+ # Password in URI (scheme://user:password@host)
56
+ re.compile(r"(?i)([a-z]+://[^:]+:)[^@\s]{4,}(@)"),
57
+ ]
58
+
59
+ _REDACTED = "[REDACTED]"
60
+
61
+
62
+ def _is_sensitive_key(key: str) -> bool:
63
+ """Whether a dict key name suggests a secret value."""
64
+ return bool(_SENSITIVE_KEY_PATTERNS.search(key))
65
+
66
+
67
+ def _redact_value_pattern(value: str) -> str:
68
+ """Replace known secret patterns inside a string."""
69
+ result = value
70
+ for pattern in _SENSITIVE_VALUE_PATTERNS:
71
+ if pattern.pattern.startswith("(?i)([a-z]+://"):
72
+ # URI password pattern — preserve scheme and host
73
+ result = pattern.sub(rf"\1{_REDACTED}\2", result)
74
+ else:
75
+ result = pattern.sub(_REDACTED, result)
76
+ return result
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # Redaction guard
81
+ # ---------------------------------------------------------------------------
82
+
83
+
84
+ class RedactionGuard:
85
+ """Centralized redaction engine.
86
+
87
+ Maintains a set of known secret values (resolved from the secrets
88
+ store) and applies key-name + value-pattern matching.
89
+ """
90
+
91
+ def __init__(self) -> None:
92
+ self._known_values: set[str] = set()
93
+
94
+ def load_secrets(self, names: set[str]) -> None:
95
+ """Resolve secret values by name and cache them for redaction.
96
+
97
+ Parameters
98
+ ----------
99
+ names:
100
+ Secret names to resolve from the active secrets store.
101
+ """
102
+ from voodoo.security.secrets import get
103
+
104
+ for name in names:
105
+ value = get(name)
106
+ if value and len(value) >= 4:
107
+ self._known_values.add(value)
108
+
109
+ def add_value(self, value: str) -> None:
110
+ """Manually add a known secret value for redaction."""
111
+ if value and len(value) >= 4:
112
+ self._known_values.add(value)
113
+
114
+ def redact(self, data: Any) -> Any:
115
+ """Recursively redact secrets from a data structure.
116
+
117
+ Handles dicts, lists, strings, and passthrough for other types.
118
+ """
119
+ if isinstance(data, dict):
120
+ return {k: self._redact_field(k, v) for k, v in data.items()}
121
+ if isinstance(data, list):
122
+ return [self.redact(item) for item in data]
123
+ if isinstance(data, str):
124
+ return self.redact_string(data)
125
+ return data
126
+
127
+ def _redact_field(self, key: str, value: Any) -> Any:
128
+ """Redact a single key-value pair."""
129
+ if _is_sensitive_key(key):
130
+ if isinstance(value, str) and value:
131
+ return _REDACTED
132
+ if value is not None:
133
+ return _REDACTED
134
+ return self.redact(value)
135
+
136
+ def redact_string(self, value: str) -> str:
137
+ """Redact known secret values and patterns inside a string."""
138
+ result = value
139
+ # Replace known secret values
140
+ for secret_val in self._known_values:
141
+ if secret_val in result:
142
+ result = result.replace(secret_val, _REDACTED)
143
+ # Replace common patterns
144
+ result = _redact_value_pattern(result)
145
+ return result
146
+
147
+
148
+ # ---------------------------------------------------------------------------
149
+ # Module-level singleton
150
+ # ---------------------------------------------------------------------------
151
+
152
+ _guard = RedactionGuard()
153
+
154
+
155
+ def redact(data: Any) -> Any:
156
+ """Redact secrets from data using the global guard.
157
+
158
+ Convenience wrapper around :meth:`RedactionGuard.redact`.
159
+ """
160
+ return _guard.redact(data)
161
+
162
+
163
+ def redact_string(value: str) -> str:
164
+ """Redact secrets from a string using the global guard."""
165
+ return _guard.redact_string(value)
166
+
167
+
168
+ def load_secrets(names: set[str]) -> None:
169
+ """Load secret values into the global guard for redaction."""
170
+ _guard.load_secrets(names)
171
+
172
+
173
+ def add_known_value(value: str) -> None:
174
+ """Add a known secret value to the global guard."""
175
+ _guard.add_value(value)