voodoo-framework 2.1.0__tar.gz → 2.2.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.1.0 → voodoo_framework-2.2.0}/PKG-INFO +1 -1
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/__init__.py +13 -1
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/primitives/effect.py +9 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/runtime/capability.py +54 -4
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/runtime/engine.py +23 -4
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/security/__init__.py +26 -0
- voodoo_framework-2.2.0/src/voodoo/security/redaction.py +175 -0
- voodoo_framework-2.2.0/src/voodoo/security/secrets.py +223 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo_framework.egg-info/PKG-INFO +1 -1
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo_framework.egg-info/SOURCES.txt +3 -0
- voodoo_framework-2.2.0/tests/test_capability_security.py +426 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_contract_api.py +11 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/LICENSE +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/README.md +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/pyproject.toml +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/setup.cfg +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/adapters/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/adapters/capabilities.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/adapters/registry.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/adapters/tailwind.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/adapters/voodoo_css.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/agent.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/agents/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/agents/models.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/agents/registry.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/ai/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/ai/agent.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/ai/providers/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/ai/providers/anthropic.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/ai/providers/gemini.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/ai/providers/mock.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/ai/providers/ollama.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/ai/providers/openai.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/ai/tools/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/ai/tools/registry.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/api.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/auth/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/auth/cookies.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/auth/guards.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/auth/jwt.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/auth/middleware.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/auth/passwords.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/auth/user.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/__main__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/agents.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/ai.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/approvals.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/auth.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/dev.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/doctor.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/executions.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/generate.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/inspect.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/new.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/objects.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/recover.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/routes.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/scaffolding.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/schedules.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/tasks.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/terminal.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/theme.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/cli/version.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/components.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/config.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/core/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/core/app.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/core/errors.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/core/events.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/core/render.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/core/routing.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/core/sitemap.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/core/state.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/data/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/data/base.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/data/model.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/i18n.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/mcp/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/mcp/client.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/memory/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/memory/interfaces.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/memory/sqlite.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/mesh/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/mesh/client.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/mesh/event_bus.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/primitives/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/primitives/capability.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/primitives/compute.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/primitives/constraint.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/primitives/intent.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/primitives/resource.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/primitives/state.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/primitives/time.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/py.typed +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/queue.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/routing/api.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/routing/pages.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/runtime/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/runtime/adaptive.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/runtime/constraint.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/runtime/context.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/runtime/errors.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/runtime/execution.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/runtime/graph.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/runtime/human.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/runtime/persistence.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/runtime/planner.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/runtime/scheduler.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/runtime/task.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/runtime/workflow.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/schedule.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/security/cors.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/security/csrf.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/security/headers.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/security/passwords.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/security/rate_limit.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/seo.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/static/client.js +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/status.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/cache/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/cache/interfaces.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/cache/memory.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/cache/redis.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/database/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/database/interfaces.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/database/postgres.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/database/sqlite.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/events/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/events/interfaces.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/events/local.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/events/postgres.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/events/sqlite.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/execution/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/execution/migrations.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/execution/postgres.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/execution/sqlite.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/manager.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/objects/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/objects/interfaces.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/objects/local.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/objects/s3.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/queue/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/queue/interfaces.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/queue/memory.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/queue/postgres.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/queue/redis.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/queue/sqlite.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/scheduler/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/storage/scheduler/sqlite.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/telemetry/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/telemetry/api.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/telemetry/middleware.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/telemetry/store.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/theme.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/tools/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/tools/registry.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/ui/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/ui/component.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/ui/events.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/ui/library.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/ui/markdown.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/ui/rendering.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/ui/state.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/ui/styles/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/ui/styles/presets.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/ui/styles/theme.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/workers/__init__.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo/workers/queue.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo_framework.egg-info/dependency_links.txt +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo_framework.egg-info/entry_points.txt +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo_framework.egg-info/requires.txt +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/src/voodoo_framework.egg-info/top_level.txt +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_agent.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_agent_registry.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_agent_runtime.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_app.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_auth.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_checkpoint_resume.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_chrome.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_cli.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_cli_inspect.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_components.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_config.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_data.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_design_system.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_durable_hitl.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_events.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_execution_sqlite_store.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_http_runtime.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_human.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_i18n.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_integration.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_mcp.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_mcp_runtime.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_memory.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_mesh.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_mesh_event_bus.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_model.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_object_store.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_persistence.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_planner_adaptive.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_primitives.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_provider_switching.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_providers.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_queue.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_reactive.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_runtime.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_scheduler.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_security.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_seo.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_state.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_theme.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_theme_presets.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_tools.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_ui.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.0}/tests/test_websocket.py +0 -0
- {voodoo_framework-2.1.0 → voodoo_framework-2.2.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.2.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>
|
|
@@ -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.
|
|
105
|
+
__version__ = "2.2.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",
|
|
@@ -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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Secrets store — centralized secret management (Sprint 19, ROADMAP §55).
|
|
2
|
+
|
|
3
|
+
Provides a single interface for accessing secrets so that:
|
|
4
|
+
|
|
5
|
+
- Secrets are never hardcoded or leaked into events/journal/telemetry.
|
|
6
|
+
- The default backend reads from environment variables (zero-config).
|
|
7
|
+
- An optional encrypted local store is available for development.
|
|
8
|
+
- Provider managers (Vault, AWS Secrets Manager, etc.) are future adapters.
|
|
9
|
+
|
|
10
|
+
Usage::
|
|
11
|
+
|
|
12
|
+
from voodoo.security.secrets import secrets
|
|
13
|
+
|
|
14
|
+
api_key = secrets.get("OPENAI_API_KEY")
|
|
15
|
+
db_pass = secrets.get("DATABASE_PASSWORD")
|
|
16
|
+
|
|
17
|
+
The module-level ``secrets`` singleton is an :class:`EnvSecretStore` by
|
|
18
|
+
default. Call ``configure()`` to swap the backend.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Protocol, runtime_checkable
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"SecretStore",
|
|
30
|
+
"EnvSecretStore",
|
|
31
|
+
"LocalSecretStore",
|
|
32
|
+
"SecretsError",
|
|
33
|
+
"secrets",
|
|
34
|
+
"configure",
|
|
35
|
+
"registered_names",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SecretsError(RuntimeError):
|
|
40
|
+
"""Raised when a secret cannot be retrieved."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Protocol
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@runtime_checkable
|
|
49
|
+
class SecretStore(Protocol):
|
|
50
|
+
"""Minimal interface every secrets backend must satisfy."""
|
|
51
|
+
|
|
52
|
+
def get(self, name: str) -> str | None:
|
|
53
|
+
"""Return the secret value, or ``None`` if not found."""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
def has(self, name: str) -> bool:
|
|
57
|
+
"""Whether the secret exists."""
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Environment-variable backend (default)
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class EnvSecretStore:
|
|
67
|
+
"""Read secrets from environment variables.
|
|
68
|
+
|
|
69
|
+
This is the zero-config default — no external services required.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def get(self, name: str) -> str | None:
|
|
73
|
+
return os.environ.get(name)
|
|
74
|
+
|
|
75
|
+
def has(self, name: str) -> bool:
|
|
76
|
+
return name in os.environ
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# Encrypted local file backend
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class LocalSecretStore:
|
|
85
|
+
"""Encrypted local file store using Fernet symmetric encryption.
|
|
86
|
+
|
|
87
|
+
Requires the ``cryptography`` package (optional extra ``[secrets]``).
|
|
88
|
+
The encryption key is derived from a passphrase or read from a key file.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
path:
|
|
93
|
+
Path to the secrets JSON file.
|
|
94
|
+
key:
|
|
95
|
+
Fernet encryption key. If ``None``, reads from ``VOODOO_SECRETS_KEY``
|
|
96
|
+
env var or ``.voodoo-secrets-key`` file.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(self, path: str | Path, key: str | None = None) -> None:
|
|
100
|
+
self._path = Path(path)
|
|
101
|
+
self._key = key
|
|
102
|
+
self._data: dict[str, str] | None = None
|
|
103
|
+
|
|
104
|
+
def _ensure_key(self) -> str:
|
|
105
|
+
if self._key:
|
|
106
|
+
return self._key
|
|
107
|
+
env_key = os.environ.get("VOODOO_SECRETS_KEY")
|
|
108
|
+
if env_key:
|
|
109
|
+
self._key = env_key
|
|
110
|
+
return env_key
|
|
111
|
+
key_file = Path(".voodoo-secrets-key")
|
|
112
|
+
if key_file.exists():
|
|
113
|
+
self._key = key_file.read_text().strip()
|
|
114
|
+
return self._key
|
|
115
|
+
raise SecretsError(
|
|
116
|
+
"No encryption key found. Set VOODOO_SECRETS_KEY env var, "
|
|
117
|
+
"create .voodoo-secrets-key file, or pass key= to LocalSecretStore."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def _load(self) -> dict[str, str]:
|
|
121
|
+
if self._data is not None:
|
|
122
|
+
return self._data
|
|
123
|
+
if not self._path.exists():
|
|
124
|
+
self._data = {}
|
|
125
|
+
return self._data
|
|
126
|
+
try:
|
|
127
|
+
from cryptography.fernet import Fernet
|
|
128
|
+
|
|
129
|
+
key = self._ensure_key()
|
|
130
|
+
f = Fernet(key.encode() if isinstance(key, str) else key)
|
|
131
|
+
raw = self._path.read_bytes()
|
|
132
|
+
decrypted = f.decrypt(raw)
|
|
133
|
+
self._data = json.loads(decrypted)
|
|
134
|
+
except ImportError:
|
|
135
|
+
# cryptography not installed — read as plain JSON
|
|
136
|
+
self._data = json.loads(self._path.read_text())
|
|
137
|
+
except SecretsError:
|
|
138
|
+
# No key available — fall back to plain JSON
|
|
139
|
+
self._data = json.loads(self._path.read_text())
|
|
140
|
+
except Exception: # noqa: BLE001
|
|
141
|
+
# Decryption failed — try plain JSON (dev convenience)
|
|
142
|
+
self._data = json.loads(self._path.read_text())
|
|
143
|
+
return self._data
|
|
144
|
+
|
|
145
|
+
def get(self, name: str) -> str | None:
|
|
146
|
+
return self._load().get(name)
|
|
147
|
+
|
|
148
|
+
def has(self, name: str) -> bool:
|
|
149
|
+
return name in self._load()
|
|
150
|
+
|
|
151
|
+
def set(self, name: str, value: str) -> None:
|
|
152
|
+
"""Store a secret (encrypts and writes to disk)."""
|
|
153
|
+
data = self._load()
|
|
154
|
+
data[name] = value
|
|
155
|
+
self._save(data)
|
|
156
|
+
|
|
157
|
+
def _save(self, data: dict[str, str]) -> None:
|
|
158
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
try:
|
|
160
|
+
from cryptography.fernet import Fernet
|
|
161
|
+
|
|
162
|
+
key = self._ensure_key()
|
|
163
|
+
f = Fernet(key.encode() if isinstance(key, str) else key)
|
|
164
|
+
encrypted = f.encrypt(json.dumps(data).encode())
|
|
165
|
+
self._path.write_bytes(encrypted)
|
|
166
|
+
except (ImportError, SecretsError):
|
|
167
|
+
self._path.write_text(json.dumps(data, indent=2))
|
|
168
|
+
self._data = data
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
# Module-level singleton
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
#: The active secrets store. Defaults to :class:`EnvSecretStore`.
|
|
176
|
+
_store: SecretStore = EnvSecretStore()
|
|
177
|
+
|
|
178
|
+
#: Names of secrets that have been explicitly registered for redaction.
|
|
179
|
+
_registered_names: set[str] = set()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def configure(store: SecretStore) -> None:
|
|
183
|
+
"""Swap the active secrets backend.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
store:
|
|
188
|
+
Any object satisfying the :class:`SecretStore` protocol.
|
|
189
|
+
"""
|
|
190
|
+
global _store
|
|
191
|
+
if not isinstance(store, SecretStore):
|
|
192
|
+
raise TypeError(
|
|
193
|
+
f"store must satisfy the SecretStore protocol, got {type(store).__name__}"
|
|
194
|
+
)
|
|
195
|
+
_store = store
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get(name: str) -> str | None:
|
|
199
|
+
"""Retrieve a secret by name from the active store."""
|
|
200
|
+
return _store.get(name)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def has(name: str) -> bool:
|
|
204
|
+
"""Whether a secret exists in the active store."""
|
|
205
|
+
return _store.has(name)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def register_name(name: str) -> None:
|
|
209
|
+
"""Register a secret name for redaction tracking.
|
|
210
|
+
|
|
211
|
+
Registered names (and their values, when resolved) are scrubbed
|
|
212
|
+
from events, journal entries, telemetry, and log output.
|
|
213
|
+
"""
|
|
214
|
+
_registered_names.add(name)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def registered_names() -> set[str]:
|
|
218
|
+
"""Return the set of registered secret names."""
|
|
219
|
+
return set(_registered_names)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# Convenience alias
|
|
223
|
+
secrets = EnvSecretStore()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voodoo-framework
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.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>
|
|
@@ -110,6 +110,8 @@ src/voodoo/security/csrf.py
|
|
|
110
110
|
src/voodoo/security/headers.py
|
|
111
111
|
src/voodoo/security/passwords.py
|
|
112
112
|
src/voodoo/security/rate_limit.py
|
|
113
|
+
src/voodoo/security/redaction.py
|
|
114
|
+
src/voodoo/security/secrets.py
|
|
113
115
|
src/voodoo/static/client.js
|
|
114
116
|
src/voodoo/storage/__init__.py
|
|
115
117
|
src/voodoo/storage/manager.py
|
|
@@ -171,6 +173,7 @@ tests/test_agent_registry.py
|
|
|
171
173
|
tests/test_agent_runtime.py
|
|
172
174
|
tests/test_app.py
|
|
173
175
|
tests/test_auth.py
|
|
176
|
+
tests/test_capability_security.py
|
|
174
177
|
tests/test_checkpoint_resume.py
|
|
175
178
|
tests/test_chrome.py
|
|
176
179
|
tests/test_cli.py
|