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.
- {voodoo_framework-2.5.2/src/voodoo_framework.egg-info → voodoo_framework-2.6.0}/PKG-INFO +13 -1
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/README.md +10 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/pyproject.toml +3 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/__init__.py +1 -1
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/config.py +82 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/core/app.py +47 -0
- voodoo_framework-2.6.0/src/voodoo/edge/__init__.py +122 -0
- voodoo_framework-2.6.0/src/voodoo/edge/auth.py +243 -0
- voodoo_framework-2.6.0/src/voodoo/edge/errors.py +181 -0
- voodoo_framework-2.6.0/src/voodoo/edge/gateway.py +556 -0
- voodoo_framework-2.6.0/src/voodoo/edge/http.py +316 -0
- voodoo_framework-2.6.0/src/voodoo/edge/models.py +309 -0
- voodoo_framework-2.6.0/src/voodoo/edge/mqtt.py +239 -0
- voodoo_framework-2.6.0/src/voodoo/edge/protocol.py +282 -0
- voodoo_framework-2.6.0/src/voodoo/edge/simulator.py +341 -0
- voodoo_framework-2.6.0/src/voodoo/edge/store.py +835 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0/src/voodoo_framework.egg-info}/PKG-INFO +13 -1
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo_framework.egg-info/SOURCES.txt +14 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo_framework.egg-info/requires.txt +3 -0
- voodoo_framework-2.6.0/tests/test_edge_device.py +325 -0
- voodoo_framework-2.6.0/tests/test_edge_gateway.py +417 -0
- voodoo_framework-2.6.0/tests/test_edge_http.py +293 -0
- voodoo_framework-2.6.0/tests/test_edge_protocol.py +159 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/LICENSE +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/setup.cfg +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/adapters/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/adapters/capabilities.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/adapters/registry.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/adapters/tailwind.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/adapters/voodoo_css.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/agent.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/agents/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/agents/models.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/agents/registry.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ai/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ai/agent.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ai/providers/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ai/providers/anthropic.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ai/providers/gemini.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ai/providers/mock.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ai/providers/ollama.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ai/providers/openai.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ai/tools/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ai/tools/registry.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/api.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/auth/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/auth/cookies.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/auth/guards.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/auth/jwt.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/auth/middleware.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/auth/passwords.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/auth/user.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/__main__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/agents.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/ai.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/approvals.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/auth.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/create.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/dev.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/doctor.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/executions.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/generate.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/inspect.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/new.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/objects.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/protocol.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/recover.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/routes.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/scaffolding.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/schedules.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/status.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/tasks.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/terminal.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/theme.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/version.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/cli/workers_cmd.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/components.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/core/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/core/errors.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/core/events.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/core/render.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/core/routing.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/core/sitemap.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/core/state.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/data/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/data/base.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/data/model.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/i18n.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/mcp/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/mcp/client.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/memory/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/memory/interfaces.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/memory/sqlite.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/mesh/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/mesh/client.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/mesh/event_bus.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/primitives/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/primitives/capability.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/primitives/compute.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/primitives/constraint.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/primitives/effect.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/primitives/intent.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/primitives/resource.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/primitives/state.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/primitives/time.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/protocol/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/protocol/export.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/protocol/schemas.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/py.typed +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/queue.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/routing/api.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/routing/pages.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/adaptive.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/capability.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/constraint.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/context.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/engine.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/errors.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/execution.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/graph.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/human.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/persistence.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/planner.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/scheduler.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/task.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/runtime/workflow.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/schedule.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/security/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/security/cors.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/security/csrf.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/security/headers.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/security/passwords.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/security/rate_limit.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/security/redaction.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/security/secrets.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/seo.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/static/client.js +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/status.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/cache/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/cache/interfaces.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/cache/memory.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/cache/redis.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/database/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/database/interfaces.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/database/postgres.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/database/sqlite.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/events/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/events/interfaces.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/events/local.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/events/postgres.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/events/sqlite.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/execution/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/execution/migrations.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/execution/postgres.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/execution/sqlite.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/manager.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/objects/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/objects/interfaces.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/objects/local.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/objects/s3.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/queue/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/queue/interfaces.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/queue/memory.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/queue/postgres.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/queue/redis.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/queue/sqlite.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/scheduler/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/storage/scheduler/sqlite.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/telemetry/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/telemetry/api.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/telemetry/middleware.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/telemetry/otlp.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/telemetry/store.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/theme.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/tools/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/tools/registry.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ui/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ui/component.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ui/events.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ui/library.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ui/markdown.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ui/rendering.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ui/state.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ui/styles/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ui/styles/presets.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/ui/styles/theme.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/workers/__init__.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo/workers/queue.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo_framework.egg-info/dependency_links.txt +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo_framework.egg-info/entry_points.txt +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/src/voodoo_framework.egg-info/top_level.txt +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_agent.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_agent_registry.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_agent_runtime.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_app.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_auth.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_capability_security.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_checkpoint_resume.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_chrome.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_cli.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_cli_inspect.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_components.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_config.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_contract_api.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_create.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_data.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_design_system.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_durable_hitl.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_events.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_execution_sqlite_store.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_http_runtime.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_human.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_i18n.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_integration.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_mcp.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_mcp_runtime.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_memory.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_mesh.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_mesh_event_bus.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_model.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_object_store.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_observability.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_persistence.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_planner_adaptive.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_primitives.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_protocol.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_provider_switching.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_providers.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_queue.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_reactive.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_runtime.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_scheduler.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_security.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_seo.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_state.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_theme.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_theme_presets.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_tools.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_ui.py +0 -0
- {voodoo_framework-2.5.2 → voodoo_framework-2.6.0}/tests/test_websocket.py +0 -0
- {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.
|
|
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)
|
|
@@ -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
|