microcoreos 0.3.2__tar.gz → 0.4.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.
- {microcoreos-0.3.2 → microcoreos-0.4.0}/AI_CONTEXT.md +4 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/PKG-INFO +4 -4
- {microcoreos-0.3.2 → microcoreos-0.4.0}/README.md +3 -3
- microcoreos-0.4.0/extras/available_domains/users/migrations/001_create_users.sql +11 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/postgresql/postgresql_tool.py +43 -7
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/postgresql/tests/postgresql_describe_schema_test.py +25 -6
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/postgresql/tests/postgresql_tool_test.py +21 -2
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/scaffold.py +74 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/pyproject.toml +1 -1
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/helpers/active_db.py +18 -2
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/db/test_db_parity.py +52 -13
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/http_server/test_http_pipeline.py +57 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/http_server/context.py +24 -1
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/http_server/http_server_tool.py +13 -1
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/http_server/pipeline.py +39 -1
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/sqlite/migrations.py +22 -5
- {microcoreos-0.3.2 → microcoreos-0.4.0}/uv.lock +1 -1
- microcoreos-0.3.2/extras/available_domains/users/migrations/001_create_users.sql +0 -12
- {microcoreos-0.3.2 → microcoreos-0.4.0}/.agent/skills/microcoreos-architecture/SKILL.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/.agent/skills/microcoreos-architecture/agent.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/.agent/workflows/feature-plan.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/.agent/workflows/multi-domain-plan.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/.agent/workflows/new-domain.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/.agent/workflows/new-tool.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/.claude/settings.local.json +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/.dockerignore +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/.env.example +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/.github/workflows/ci.yml +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/.github/workflows/release.yml +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/.gitignore +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/.mutmut-cache +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/.python-version +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/AGENTS.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/Dockerfile +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/INSTRUCTIONS_FOR_AI.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/LICENSE +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/ROADMAP.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/cli.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/conftest.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/dev_infra/cache_probe.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/dev_infra/docker-compose.yml +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/docs/CLI.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/docs/CORE_INFRASTRUCTURE.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/docs/ELASTIC_DEPLOYMENT.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/docs/EVENT_BUS.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/docs/HTTP_SERVER.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/docs/INDEX.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/docs/OBSERVABILITY.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/docs/OBSERVABILITY_API.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/docs/PARALLEL_DEVELOPMENT.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/docs/translations/es/README.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/devtools/lint/plugin_sources.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/devtools/plugins/discovery_naming_linter_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/devtools/plugins/doc_path_linter_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/devtools/plugins/domain_isolation_linter_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/devtools/plugins/event_contract_linter_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/devtools/plugins/event_schemas_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/devtools/plugins/field_divergence_linter_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/devtools/plugins/route_collision_linter_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/devtools/plugins/table_ownership_linter_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/devtools/plugins/tool_doc_drift_linter_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/system/plugins/event_delivery_monitor_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/system/plugins/system_events_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/system/plugins/system_events_stream_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/system/plugins/system_logs_stream_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/system/plugins/system_metrics_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/system/plugins/system_status_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/system/plugins/system_traces_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/system/plugins/system_traces_stream_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/system/plugins/tool_health_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/chaos/plugins/blocking_boot_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/chaos/plugins/chaos_control_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/chaos/plugins/failing_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/chaos/plugins/stress_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/ping/plugins/ping_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/scheduler/migrations/001_scheduler_one_shots.sql +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/scheduler/models/scheduler_one_shot.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/scheduler/plugins/durable_one_shots_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/users/models/user.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/users/plugins/create_user_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/users/plugins/delete_user_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/users/plugins/get_me_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/users/plugins/get_user_by_id_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/users/plugins/get_users_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/users/plugins/login_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/users/plugins/logout_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/users/plugins/update_user_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/users/plugins/welcome_service_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/auth/auth_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/auth/tests/auth_tool_test.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/chaos/chaos_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/kafka/kafka_driver.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/rabbitmq/rabbitmq_driver.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/redis_state/redis_state_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/s3/__init__.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/s3/s3_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/s3/tests/s3_tool_test.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/scheduler/scheduler_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/scheduler/tests/scheduler_singleton_test.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/scheduler/tests/scheduler_tool_test.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/hatch_build.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/main.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/__init__.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/base_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/base_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/catalog.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/cli.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/container.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/context.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/kernel.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/project.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/project_readme.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/registry.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/upgrade.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos_dev/__init__.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos_dev/cli.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos_dev/fuzzer.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos_dev/pipeline.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos_dev/plan/__init__.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos_dev/plan/rules.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos_dev/plan/scan.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos_dev/plan/schema.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos_dev/probe.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/plans/README.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/plans/active_plan.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/plans/active_plan.yaml +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/core/test_core.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/core/test_core_purity.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/core/test_kernel.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/core/test_no_retry.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/core/test_plugin_di_fixtures.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/core/test_registry_collisions.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/core/test_tool_proxy.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/dev/corpus/README.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/dev/corpus/qwen_twitter_plan.yaml +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/dev/test_pipeline.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/dev/test_plan_validator.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/domains/ping/test_ping_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/domains/users/test_create_user_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/domains/users/test_delete_user_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/domains/users/test_get_me_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/domains/users/test_get_user_by_id_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/domains/users/test_get_users_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/domains/users/test_login_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/domains/users/test_logout_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/domains/users/test_update_user_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/domains/users/test_welcome_service_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/helpers/async_wait.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/helpers/mock_db.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/helpers/trace_chains.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/linters/test_discovery_naming_linter.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/linters/test_doc_path_linter.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/linters/test_domain_isolation_linter.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/linters/test_event_contract_linter.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/linters/test_field_divergence_linter.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/linters/test_route_collision_linter.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/linters/test_table_ownership_linter.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/linters/test_tool_doc_drift_linter.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_catalog.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_chaos_control.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_chaos_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_cli.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_durable_one_shots.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_event_schemas_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_scaffold.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_system_events_stats.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_system_traces_plugin.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_trace_chain_helper.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_upgrade.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/config/test_config_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/context/test_context_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/event_bus/test_event_bus_broker_parity.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/event_bus/test_event_bus_groups.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/event_bus/test_event_bus_kafka_parity.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/event_bus/test_event_bus_rabbitmq_parity.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/event_bus/test_event_bus_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/event_bus/test_redis_streams_driver.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/http_server/test_http_params_hardening.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/http_server/test_http_server_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/http_server/test_security_hardening.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/logger/test_logger_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/registry/test_registry_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/s3/test_s3_parity.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/sqlite/test_sqlite_concurrency.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/sqlite/test_sqlite_describe_schema.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/sqlite/test_sqlite_driver.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/sqlite/test_sqlite_migrations.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/sqlite/test_sqlite_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/state/test_state_parity.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/state/test_state_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/telemetry/test_telemetry_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/config/config_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/context/authoring_guide.md +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/context/context_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/context/renderers.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/context/scanners.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/event_bus/drivers.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/event_bus/envelope.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/event_bus/event_bus_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/event_bus/redis_streams_driver.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/event_bus/sqlite_driver.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/http_server/types.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/logger/logger_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/sqlite/errors.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/sqlite/sqlite_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/sqlite/transaction.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/state/state_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/system/registry_tool.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/telemetry/__init__.py +0 -0
- {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/telemetry/telemetry_tool.py +0 -0
|
@@ -63,6 +63,10 @@ HTTP Server Tool (http):
|
|
|
63
63
|
- context.set_cookie(key, value, max_age=3600, ...): Set secure response cookie.
|
|
64
64
|
- context.set_header(key, value): Add custom response header.
|
|
65
65
|
- context.set_binary_response(content: bytes, media_type: str): Return raw file.
|
|
66
|
+
- context.client_ip: Best-effort caller IP (property). Raw signal only — the
|
|
67
|
+
plugin decides what to do with it (e.g. state.increment() keyed by IP for
|
|
68
|
+
an identity-aware business rule). Never security-authoritative on its own;
|
|
69
|
+
see context.py's client_ip docstring for the trust order and its limits.
|
|
66
70
|
- RESPONSE CONTRACT:
|
|
67
71
|
- Standard: return {"success": bool, "data": ..., "error": ...}
|
|
68
72
|
- WARNING: All values in 'data' must be JSON-serializable. Pydantic model
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microcoreos
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Atomic Microkernel Architecture optimized for AI-Driven Development
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -204,9 +204,9 @@ The system scans all tools, plugins, domains, events, and models — then writes
|
|
|
204
204
|
|
|
205
205
|
A Tool is identified by its `name` property. The SQLite tool and the PostgreSQL tool both expose the same API (`query`, `execute`, `transaction`, `health_check`) and both use `$1, $2` placeholders. To switch databases:
|
|
206
206
|
|
|
207
|
-
1.
|
|
208
|
-
2.
|
|
209
|
-
3. Plugins don't change a single line.
|
|
207
|
+
1. Move `tools/sqlite` to `extras/available_tools/sqlite`
|
|
208
|
+
2. Move `extras/available_tools/postgresql` to `tools/postgresql`
|
|
209
|
+
3. Plugins don't change a single line. Both tools expose the exact same `db` API.
|
|
210
210
|
|
|
211
211
|
Migrations are the honest exception: the `db` tool is not an ORM — SQL files run
|
|
212
212
|
**verbatim** on the active engine (no dialect translation), so an engine swap includes
|
|
@@ -163,9 +163,9 @@ The system scans all tools, plugins, domains, events, and models — then writes
|
|
|
163
163
|
|
|
164
164
|
A Tool is identified by its `name` property. The SQLite tool and the PostgreSQL tool both expose the same API (`query`, `execute`, `transaction`, `health_check`) and both use `$1, $2` placeholders. To switch databases:
|
|
165
165
|
|
|
166
|
-
1.
|
|
167
|
-
2.
|
|
168
|
-
3. Plugins don't change a single line.
|
|
166
|
+
1. Move `tools/sqlite` to `extras/available_tools/sqlite`
|
|
167
|
+
2. Move `extras/available_tools/postgresql` to `tools/postgresql`
|
|
168
|
+
3. Plugins don't change a single line. Both tools expose the exact same `db` API.
|
|
169
169
|
|
|
170
170
|
Migrations are the honest exception: the `db` tool is not an ORM — SQL files run
|
|
171
171
|
**verbatim** on the active engine (no dialect translation), so an engine swap includes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- users table (auth domain). Written explicitly for SQLite default.
|
|
2
|
+
-- On a PostgreSQL swap, rewrite the autoincrement primary key:
|
|
3
|
+
-- Change "INTEGER PRIMARY KEY" -> "INTEGER PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY"
|
|
4
|
+
-- (or "SERIAL PRIMARY KEY"). See docs/ELASTIC_DEPLOYMENT.md Stage 1.
|
|
5
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
6
|
+
id INTEGER PRIMARY KEY,
|
|
7
|
+
name TEXT NOT NULL,
|
|
8
|
+
email TEXT NOT NULL UNIQUE,
|
|
9
|
+
password_hash TEXT NOT NULL,
|
|
10
|
+
roles TEXT NOT NULL DEFAULT '["user"]'
|
|
11
|
+
);
|
{microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/postgresql/postgresql_tool.py
RENAMED
|
@@ -39,12 +39,35 @@ ERROR CONTRACT (part of the gold standard — any db tool MUST match it):
|
|
|
39
39
|
PLACEHOLDERS: PostgreSQL uses $1, $2, $3... (NOT '?' like SQLite).
|
|
40
40
|
"""
|
|
41
41
|
|
|
42
|
+
import asyncio
|
|
43
|
+
import json
|
|
42
44
|
import os
|
|
43
45
|
import re
|
|
44
|
-
|
|
46
|
+
|
|
45
47
|
import asyncpg
|
|
48
|
+
|
|
46
49
|
from microcoreos import BaseTool, ToolUnavailableError
|
|
47
50
|
|
|
51
|
+
_CREATE_TABLE_RE = re.compile(
|
|
52
|
+
r"CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[\"'`]?(\w+)[\"'`]?", re.IGNORECASE
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def _init_postgres_connection(conn: asyncpg.Connection) -> None:
|
|
57
|
+
"""Register JSON/JSONB codecs so asyncpg automatically serializes/deserializes Python dicts and lists."""
|
|
58
|
+
await conn.set_type_codec(
|
|
59
|
+
"json",
|
|
60
|
+
encoder=json.dumps,
|
|
61
|
+
decoder=json.loads,
|
|
62
|
+
schema="pg_catalog",
|
|
63
|
+
)
|
|
64
|
+
await conn.set_type_codec(
|
|
65
|
+
"jsonb",
|
|
66
|
+
encoder=json.dumps,
|
|
67
|
+
decoder=json.loads,
|
|
68
|
+
schema="pg_catalog",
|
|
69
|
+
)
|
|
70
|
+
|
|
48
71
|
|
|
49
72
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
50
73
|
# EXCEPTIONS
|
|
@@ -334,7 +357,7 @@ class PostgresqlTool(BaseTool):
|
|
|
334
357
|
|
|
335
358
|
@property
|
|
336
359
|
def name(self) -> str:
|
|
337
|
-
return "
|
|
360
|
+
return "db"
|
|
338
361
|
|
|
339
362
|
# ─── CONSTRUCTOR ──────────────────────────────────────
|
|
340
363
|
#
|
|
@@ -377,6 +400,7 @@ class PostgresqlTool(BaseTool):
|
|
|
377
400
|
max_size=self._max_pool,
|
|
378
401
|
timeout=self._connect_timeout,
|
|
379
402
|
command_timeout=self._command_timeout,
|
|
403
|
+
init=_init_postgres_connection,
|
|
380
404
|
),
|
|
381
405
|
timeout=self._connect_timeout,
|
|
382
406
|
)
|
|
@@ -483,6 +507,8 @@ class PostgresqlTool(BaseTool):
|
|
|
483
507
|
print(f" [Migration] ⚠️ Circular dependency detected: {e}")
|
|
484
508
|
ordered_keys = sorted(migrations.keys())
|
|
485
509
|
|
|
510
|
+
live_schema = set((await self.describe_schema()).keys())
|
|
511
|
+
|
|
486
512
|
# ── 3. Apply in topological order ───────────────────────────────
|
|
487
513
|
for key in ordered_keys:
|
|
488
514
|
if key not in migrations:
|
|
@@ -492,20 +518,29 @@ class PostgresqlTool(BaseTool):
|
|
|
492
518
|
domain = info["domain"]
|
|
493
519
|
filename = info["filename"]
|
|
494
520
|
|
|
521
|
+
with open(info["path"], "r", encoding="utf-8") as f:
|
|
522
|
+
lines = f.readlines()
|
|
523
|
+
sql_script = "\n".join(line for line in lines if not line.strip().startswith("--"))
|
|
524
|
+
|
|
525
|
+
declared_tables = _CREATE_TABLE_RE.findall(sql_script)
|
|
526
|
+
|
|
495
527
|
# Check if already applied
|
|
496
528
|
already_applied = await self.query_one(
|
|
497
529
|
"SELECT 1 FROM _migrations_history WHERE domain = $1 AND filename = $2",
|
|
498
530
|
[domain, filename],
|
|
499
531
|
)
|
|
500
532
|
if already_applied:
|
|
501
|
-
|
|
533
|
+
missing_tables = [t for t in declared_tables if t not in live_schema]
|
|
534
|
+
if not missing_tables:
|
|
535
|
+
continue
|
|
536
|
+
print(f" [Migration] ⚠️ Table(s) {missing_tables} declared in {key} missing from DB despite history record. Repairing...")
|
|
537
|
+
await self.execute(
|
|
538
|
+
"DELETE FROM _migrations_history WHERE domain = $1 AND filename = $2",
|
|
539
|
+
[domain, filename],
|
|
540
|
+
)
|
|
502
541
|
|
|
503
542
|
print(f" [Migration] Applying {key}...")
|
|
504
543
|
|
|
505
|
-
with open(info["path"], "r", encoding="utf-8") as f:
|
|
506
|
-
lines = f.readlines()
|
|
507
|
-
sql_script = "\n".join(line for line in lines if not line.strip().startswith("--"))
|
|
508
|
-
|
|
509
544
|
# Each migration in its own transaction
|
|
510
545
|
async with self.transaction() as tx:
|
|
511
546
|
# asyncpg support multiple statements in execute() when no params are provided
|
|
@@ -519,6 +554,7 @@ class PostgresqlTool(BaseTool):
|
|
|
519
554
|
except Exception as e:
|
|
520
555
|
raise DatabaseError(f"Migration failed for {key}: {e}", **_classify_error(e)) from e
|
|
521
556
|
|
|
557
|
+
live_schema.update(declared_tables)
|
|
522
558
|
print(f" [Migration] ✅ Applied {key}")
|
|
523
559
|
|
|
524
560
|
# ─── LIFECYCLE: shutdown() ────────────────────────────
|
|
@@ -52,7 +52,7 @@ def pg_env(monkeypatch):
|
|
|
52
52
|
monkeypatch.setenv("PG_PORT", "5432")
|
|
53
53
|
monkeypatch.setenv("PG_USER", "postgres")
|
|
54
54
|
monkeypatch.setenv("PG_PASSWORD", "postgres")
|
|
55
|
-
monkeypatch.setenv("PG_DATABASE", "
|
|
55
|
+
monkeypatch.setenv("PG_DATABASE", "microcoreos_test")
|
|
56
56
|
monkeypatch.setenv("DB_AUTO_MIGRATE", "false")
|
|
57
57
|
|
|
58
58
|
@pytest.fixture
|
|
@@ -60,11 +60,30 @@ async def db():
|
|
|
60
60
|
tool = PostgresqlTool()
|
|
61
61
|
try:
|
|
62
62
|
await tool.setup()
|
|
63
|
-
except DatabaseConnectionError:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
except DatabaseConnectionError as e:
|
|
64
|
+
if "does not exist" in str(e).lower():
|
|
65
|
+
try:
|
|
66
|
+
import os
|
|
67
|
+
import asyncpg
|
|
68
|
+
host = os.getenv("PG_HOST", "localhost")
|
|
69
|
+
port = int(os.getenv("PG_PORT", "5432"))
|
|
70
|
+
user = os.getenv("PG_USER", "postgres")
|
|
71
|
+
password = os.getenv("PG_PASSWORD", "postgres")
|
|
72
|
+
dbname = os.getenv("PG_DATABASE", "microcoreos_test")
|
|
73
|
+
conn = await asyncpg.connect(host=host, port=port, user=user, password=password, database="postgres")
|
|
74
|
+
await conn.execute(f'CREATE DATABASE "{dbname}"')
|
|
75
|
+
await conn.close()
|
|
76
|
+
await tool.setup()
|
|
77
|
+
except Exception:
|
|
78
|
+
pytest.skip(
|
|
79
|
+
"PostgreSQL not available — "
|
|
80
|
+
"docker compose -f dev_infra/docker-compose.yml up -d postgres"
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
pytest.skip(
|
|
84
|
+
"PostgreSQL not available — "
|
|
85
|
+
"docker compose -f dev_infra/docker-compose.yml up -d postgres"
|
|
86
|
+
)
|
|
68
87
|
yield tool
|
|
69
88
|
await tool.shutdown()
|
|
70
89
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import pytest
|
|
2
3
|
|
|
3
4
|
|
|
@@ -28,7 +29,7 @@ def pg_env(monkeypatch):
|
|
|
28
29
|
monkeypatch.setenv("PG_PORT", "5432")
|
|
29
30
|
monkeypatch.setenv("PG_USER", "postgres")
|
|
30
31
|
monkeypatch.setenv("PG_PASSWORD", "postgres")
|
|
31
|
-
monkeypatch.setenv("PG_DATABASE", "
|
|
32
|
+
monkeypatch.setenv("PG_DATABASE", "microcoreos_test")
|
|
32
33
|
|
|
33
34
|
# ── Unit tests (no Docker) ──────────────────────────────────────────────────
|
|
34
35
|
|
|
@@ -52,7 +53,25 @@ def test_parse_empty():
|
|
|
52
53
|
@pytest.fixture
|
|
53
54
|
async def tool():
|
|
54
55
|
t = PostgresqlTool()
|
|
55
|
-
|
|
56
|
+
try:
|
|
57
|
+
await t.setup()
|
|
58
|
+
except Exception as e:
|
|
59
|
+
if "does not exist" in str(e).lower():
|
|
60
|
+
try:
|
|
61
|
+
import asyncpg
|
|
62
|
+
host = os.getenv("PG_HOST", "localhost")
|
|
63
|
+
port = int(os.getenv("PG_PORT", "5432"))
|
|
64
|
+
user = os.getenv("PG_USER", "postgres")
|
|
65
|
+
password = os.getenv("PG_PASSWORD", "postgres")
|
|
66
|
+
dbname = os.getenv("PG_DATABASE", "microcoreos_test")
|
|
67
|
+
conn = await asyncpg.connect(host=host, port=port, user=user, password=password, database="postgres")
|
|
68
|
+
await conn.execute(f'CREATE DATABASE "{dbname}"')
|
|
69
|
+
await conn.close()
|
|
70
|
+
await t.setup()
|
|
71
|
+
except Exception as inner_e:
|
|
72
|
+
pytest.skip(f"PostgreSQL server unreachable: {inner_e}")
|
|
73
|
+
else:
|
|
74
|
+
pytest.skip(f"PostgreSQL server unreachable: {e}")
|
|
56
75
|
yield t
|
|
57
76
|
await t.execute("DROP TABLE IF EXISTS _test")
|
|
58
77
|
await t.shutdown()
|
|
@@ -216,15 +216,89 @@ Starter pytest fixtures for MicroCoreOS application tests.
|
|
|
216
216
|
Auto-generated by `microcoreos new`.
|
|
217
217
|
\"\"\"
|
|
218
218
|
|
|
219
|
+
import inspect
|
|
220
|
+
from pathlib import Path
|
|
219
221
|
from unittest.mock import AsyncMock, MagicMock
|
|
220
222
|
import pytest
|
|
221
223
|
|
|
224
|
+
from tests.helpers.active_db import active_db as _active_db
|
|
225
|
+
|
|
226
|
+
_PROJECT_ROOT = Path(__file__).resolve().parent
|
|
227
|
+
_MIGRATION_ROOTS = (
|
|
228
|
+
_PROJECT_ROOT / "domains",
|
|
229
|
+
_PROJECT_ROOT / "extras" / "available_domains",
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _migrations_dir(domain: str) -> Path | None:
|
|
234
|
+
for root in _MIGRATION_ROOTS:
|
|
235
|
+
candidate = root / domain / "migrations"
|
|
236
|
+
if candidate.is_dir():
|
|
237
|
+
return candidate
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
async def _lifecycle(tool, method: str):
|
|
242
|
+
fn = getattr(tool, method, None)
|
|
243
|
+
if fn is None:
|
|
244
|
+
return
|
|
245
|
+
result = fn()
|
|
246
|
+
if inspect.isawaitable(result):
|
|
247
|
+
await result
|
|
248
|
+
|
|
222
249
|
|
|
223
250
|
@pytest.fixture
|
|
224
251
|
def anyio_backend():
|
|
225
252
|
return "asyncio"
|
|
226
253
|
|
|
227
254
|
|
|
255
|
+
@pytest.fixture
|
|
256
|
+
async def db(request, monkeypatch):
|
|
257
|
+
marker = request.node.get_closest_marker("migrations")
|
|
258
|
+
names = marker.args if marker else ()
|
|
259
|
+
resolved = {name: _migrations_dir(name) for name in names}
|
|
260
|
+
dirs = [d for d in resolved.values() if d is not None]
|
|
261
|
+
async with _active_db(monkeypatch, *dirs) as tool:
|
|
262
|
+
yield tool
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@pytest.fixture
|
|
266
|
+
async def event_bus():
|
|
267
|
+
from tools.event_bus.event_bus_tool import EventBusTool
|
|
268
|
+
tool = EventBusTool()
|
|
269
|
+
await _lifecycle(tool, "setup")
|
|
270
|
+
yield tool
|
|
271
|
+
await _lifecycle(tool, "shutdown")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@pytest.fixture
|
|
275
|
+
async def auth(monkeypatch):
|
|
276
|
+
from extras.available_tools.auth.auth_tool import AuthTool
|
|
277
|
+
monkeypatch.setenv("AUTH_SECRET_KEY", "test-secret-key-0123456789abcdefghij")
|
|
278
|
+
tool = AuthTool()
|
|
279
|
+
await _lifecycle(tool, "setup")
|
|
280
|
+
yield tool
|
|
281
|
+
await _lifecycle(tool, "shutdown")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@pytest.fixture
|
|
285
|
+
async def config():
|
|
286
|
+
from tools.config.config_tool import ConfigTool
|
|
287
|
+
tool = ConfigTool()
|
|
288
|
+
await _lifecycle(tool, "setup")
|
|
289
|
+
yield tool
|
|
290
|
+
await _lifecycle(tool, "shutdown")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@pytest.fixture
|
|
294
|
+
async def state():
|
|
295
|
+
from tools.state.state_tool import StateTool
|
|
296
|
+
tool = StateTool()
|
|
297
|
+
await _lifecycle(tool, "setup")
|
|
298
|
+
yield tool
|
|
299
|
+
await _lifecycle(tool, "shutdown")
|
|
300
|
+
|
|
301
|
+
|
|
228
302
|
@pytest.fixture
|
|
229
303
|
def mock_logger():
|
|
230
304
|
logger = MagicMock()
|
|
@@ -58,7 +58,7 @@ _TEST_ENV = {
|
|
|
58
58
|
"PG_PORT": "5432",
|
|
59
59
|
"PG_USER": "postgres",
|
|
60
60
|
"PG_PASSWORD": "postgres",
|
|
61
|
-
"PG_DATABASE": "
|
|
61
|
+
"PG_DATABASE": "microcoreos_test",
|
|
62
62
|
# Tests apply the migrations they need explicitly (below), so boot-time
|
|
63
63
|
# auto-migration would only add unrelated domains' tables.
|
|
64
64
|
"DB_AUTO_MIGRATE": "false",
|
|
@@ -134,7 +134,23 @@ async def active_db(monkeypatch, *migration_dirs: Path):
|
|
|
134
134
|
try:
|
|
135
135
|
await tool.setup()
|
|
136
136
|
except ToolUnavailableError as e:
|
|
137
|
-
|
|
137
|
+
if "does not exist" in str(e).lower() and tool_cls.__name__ == "PostgresqlTool":
|
|
138
|
+
try:
|
|
139
|
+
import os
|
|
140
|
+
import asyncpg
|
|
141
|
+
host = os.getenv("PG_HOST", "localhost")
|
|
142
|
+
port = int(os.getenv("PG_PORT", "5432"))
|
|
143
|
+
user = os.getenv("PG_USER", "postgres")
|
|
144
|
+
password = os.getenv("PG_PASSWORD", "postgres")
|
|
145
|
+
dbname = os.getenv("PG_DATABASE", "microcoreos_test")
|
|
146
|
+
conn = await asyncpg.connect(host=host, port=port, user=user, password=password, database="postgres")
|
|
147
|
+
await conn.execute(f'CREATE DATABASE "{dbname}"')
|
|
148
|
+
await conn.close()
|
|
149
|
+
await tool.setup()
|
|
150
|
+
except Exception:
|
|
151
|
+
pytest.skip(f"active db tool {tool_cls.__name__} is unreachable: {e}")
|
|
152
|
+
else:
|
|
153
|
+
pytest.skip(f"active db tool {tool_cls.__name__} is unreachable: {e}")
|
|
138
154
|
|
|
139
155
|
tables: list[str] = []
|
|
140
156
|
try:
|
|
@@ -47,16 +47,35 @@ async def db(request, monkeypatch):
|
|
|
47
47
|
monkeypatch.setenv("PG_PORT", "5432")
|
|
48
48
|
monkeypatch.setenv("PG_USER", "postgres")
|
|
49
49
|
monkeypatch.setenv("PG_PASSWORD", "postgres")
|
|
50
|
-
monkeypatch.setenv("PG_DATABASE", "
|
|
50
|
+
monkeypatch.setenv("PG_DATABASE", "microcoreos_test")
|
|
51
51
|
monkeypatch.setenv("DB_AUTO_MIGRATE", "false")
|
|
52
52
|
tool = PostgresqlTool()
|
|
53
53
|
try:
|
|
54
54
|
await tool.setup()
|
|
55
|
-
except PGConnectionError:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
except PGConnectionError as e:
|
|
56
|
+
if "does not exist" in str(e).lower():
|
|
57
|
+
try:
|
|
58
|
+
import os
|
|
59
|
+
import asyncpg
|
|
60
|
+
host = os.getenv("PG_HOST", "localhost")
|
|
61
|
+
port = int(os.getenv("PG_PORT", "5432"))
|
|
62
|
+
user = os.getenv("PG_USER", "postgres")
|
|
63
|
+
password = os.getenv("PG_PASSWORD", "postgres")
|
|
64
|
+
dbname = os.getenv("PG_DATABASE", "microcoreos_test")
|
|
65
|
+
conn = await asyncpg.connect(host=host, port=port, user=user, password=password, database="postgres")
|
|
66
|
+
await conn.execute(f'CREATE DATABASE "{dbname}"')
|
|
67
|
+
await conn.close()
|
|
68
|
+
await tool.setup()
|
|
69
|
+
except Exception:
|
|
70
|
+
pytest.skip(
|
|
71
|
+
"PostgreSQL not available — "
|
|
72
|
+
"docker compose -f dev_infra/docker-compose.yml up -d postgres"
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
pytest.skip(
|
|
76
|
+
"PostgreSQL not available — "
|
|
77
|
+
"docker compose -f dev_infra/docker-compose.yml up -d postgres"
|
|
78
|
+
)
|
|
60
79
|
yield tool
|
|
61
80
|
await tool.execute("DROP TABLE IF EXISTS _parity")
|
|
62
81
|
await tool.shutdown()
|
|
@@ -229,17 +248,37 @@ async def both_engines(monkeypatch):
|
|
|
229
248
|
monkeypatch.setenv("PG_PORT", "5432")
|
|
230
249
|
monkeypatch.setenv("PG_USER", "postgres")
|
|
231
250
|
monkeypatch.setenv("PG_PASSWORD", "postgres")
|
|
232
|
-
monkeypatch.setenv("PG_DATABASE", "
|
|
251
|
+
monkeypatch.setenv("PG_DATABASE", "microcoreos_test")
|
|
233
252
|
monkeypatch.setenv("DB_AUTO_MIGRATE", "false")
|
|
234
253
|
pg_tool = PostgresqlTool()
|
|
235
254
|
try:
|
|
236
255
|
await pg_tool.setup()
|
|
237
|
-
except PGConnectionError:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
256
|
+
except PGConnectionError as e:
|
|
257
|
+
if "does not exist" in str(e).lower():
|
|
258
|
+
try:
|
|
259
|
+
import os
|
|
260
|
+
import asyncpg
|
|
261
|
+
host = os.getenv("PG_HOST", "localhost")
|
|
262
|
+
port = int(os.getenv("PG_PORT", "5432"))
|
|
263
|
+
user = os.getenv("PG_USER", "postgres")
|
|
264
|
+
password = os.getenv("PG_PASSWORD", "postgres")
|
|
265
|
+
dbname = os.getenv("PG_DATABASE", "microcoreos_test")
|
|
266
|
+
conn = await asyncpg.connect(host=host, port=port, user=user, password=password, database="postgres")
|
|
267
|
+
await conn.execute(f'CREATE DATABASE "{dbname}"')
|
|
268
|
+
await conn.close()
|
|
269
|
+
await pg_tool.setup()
|
|
270
|
+
except Exception:
|
|
271
|
+
await sqlite_tool.shutdown()
|
|
272
|
+
pytest.skip(
|
|
273
|
+
"PostgreSQL not available — "
|
|
274
|
+
"docker compose -f dev_infra/docker-compose.yml up -d postgres"
|
|
275
|
+
)
|
|
276
|
+
else:
|
|
277
|
+
await sqlite_tool.shutdown()
|
|
278
|
+
pytest.skip(
|
|
279
|
+
"PostgreSQL not available — "
|
|
280
|
+
"docker compose -f dev_infra/docker-compose.yml up -d postgres"
|
|
281
|
+
)
|
|
243
282
|
|
|
244
283
|
await pg_tool.execute("DROP TABLE IF EXISTS _parity_schema")
|
|
245
284
|
for tool in (sqlite_tool, pg_tool):
|
|
@@ -8,6 +8,7 @@ from tools.http_server.pipeline import (
|
|
|
8
8
|
_process_request,
|
|
9
9
|
_extract_bearer_token,
|
|
10
10
|
_extract_ws_token,
|
|
11
|
+
_extract_client_ip,
|
|
11
12
|
)
|
|
12
13
|
from tools.http_server.context import HttpContext
|
|
13
14
|
from pydantic import BaseModel
|
|
@@ -62,6 +63,62 @@ async def test_extract_bearer_token_csrf_block():
|
|
|
62
63
|
assert token == "cookie_jwt"
|
|
63
64
|
|
|
64
65
|
|
|
66
|
+
def test_extract_client_ip_trust_order():
|
|
67
|
+
# 1. Cf-Connecting-Ip wins even if the other two are also present.
|
|
68
|
+
req = MagicMock(spec=Request)
|
|
69
|
+
req.headers = Headers({
|
|
70
|
+
"Cf-Connecting-Ip": "203.0.113.7",
|
|
71
|
+
"X-Forwarded-For": "198.51.100.1, 10.0.0.1",
|
|
72
|
+
})
|
|
73
|
+
req.client = MagicMock(host="10.0.0.99")
|
|
74
|
+
assert _extract_client_ip(req) == "203.0.113.7"
|
|
75
|
+
|
|
76
|
+
# 2. No Cf-Connecting-Ip -> first hop of X-Forwarded-For.
|
|
77
|
+
req = MagicMock(spec=Request)
|
|
78
|
+
req.headers = Headers({"X-Forwarded-For": "198.51.100.1, 10.0.0.1"})
|
|
79
|
+
req.client = MagicMock(host="10.0.0.99")
|
|
80
|
+
assert _extract_client_ip(req) == "198.51.100.1"
|
|
81
|
+
|
|
82
|
+
# 3. Neither header -> the direct TCP peer.
|
|
83
|
+
req = MagicMock(spec=Request)
|
|
84
|
+
req.headers = Headers({})
|
|
85
|
+
req.client = MagicMock(host="10.0.0.99")
|
|
86
|
+
assert _extract_client_ip(req) == "10.0.0.99"
|
|
87
|
+
|
|
88
|
+
# 4. No signal at all (request.client itself is None) -> None, not a crash.
|
|
89
|
+
req = MagicMock(spec=Request)
|
|
90
|
+
req.headers = Headers({})
|
|
91
|
+
req.client = None
|
|
92
|
+
assert _extract_client_ip(req) is None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@pytest.mark.anyio
|
|
96
|
+
async def test_process_request_exposes_client_ip_to_handler():
|
|
97
|
+
req = MagicMock(spec=Request)
|
|
98
|
+
req.query_params = {}
|
|
99
|
+
req.path_params = {}
|
|
100
|
+
req.headers = Headers({"Cf-Connecting-Ip": "203.0.113.7"})
|
|
101
|
+
req.method = "GET"
|
|
102
|
+
req.url.path = "/whoami"
|
|
103
|
+
req.client = MagicMock(host="10.0.0.99")
|
|
104
|
+
|
|
105
|
+
seen_ip = {}
|
|
106
|
+
|
|
107
|
+
async def handler(data, ctx: HttpContext):
|
|
108
|
+
seen_ip["value"] = ctx.client_ip
|
|
109
|
+
return {"success": True}
|
|
110
|
+
|
|
111
|
+
await _process_request(
|
|
112
|
+
request=req,
|
|
113
|
+
body_data=None,
|
|
114
|
+
handler=handler,
|
|
115
|
+
auth_validator=None,
|
|
116
|
+
paused_owners=set(),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
assert seen_ip["value"] == "203.0.113.7"
|
|
120
|
+
|
|
121
|
+
|
|
65
122
|
@pytest.mark.anyio
|
|
66
123
|
async def test_process_request_redirect_response():
|
|
67
124
|
req = MagicMock(spec=Request)
|
|
@@ -22,11 +22,34 @@ class HttpContext:
|
|
|
22
22
|
All mutations are applied to the response before it is sent to the client.
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
-
def __init__(self) -> None:
|
|
25
|
+
def __init__(self, client_ip: str | None = None) -> None:
|
|
26
26
|
self._status_code: int = 200
|
|
27
27
|
self._status_explicit: bool = False
|
|
28
28
|
self._cookies: list[dict] = []
|
|
29
29
|
self._headers: dict[str, str] = {}
|
|
30
|
+
self._client_ip = client_ip
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def client_ip(self) -> str | None:
|
|
34
|
+
"""
|
|
35
|
+
Best-effort real caller IP — a raw signal for whatever policy the
|
|
36
|
+
PLUGIN decides (identity-aware rate limiting, audit logging,
|
|
37
|
+
fraud/abuse heuristics...). This tool never interprets it: per
|
|
38
|
+
INSTRUCTIONS_FOR_AI.md's Rate Limiting Pattern, volumetric/anonymous
|
|
39
|
+
IP throttling belongs at the edge (reverse proxy / CDN), never in
|
|
40
|
+
the monolith — but a business rule that happens to key on IP (e.g.
|
|
41
|
+
"no more than N accounts created from the same address") is exactly
|
|
42
|
+
the kind of identity-aware policy that pattern says belongs in the
|
|
43
|
+
plugin, with the `state` tool primitive. This property is what makes
|
|
44
|
+
that possible; it does not pick a policy itself.
|
|
45
|
+
|
|
46
|
+
Trust order (see pipeline.py's _extract_client_ip): Cf-Connecting-Ip,
|
|
47
|
+
then X-Forwarded-For's first hop, then the direct TCP peer. The first
|
|
48
|
+
two are only as trustworthy as whatever reverse proxy sets them —
|
|
49
|
+
this tool cannot verify one is actually in front of it. None if no
|
|
50
|
+
signal was available at all (e.g. request.client itself is None).
|
|
51
|
+
"""
|
|
52
|
+
return self._client_ip
|
|
30
53
|
|
|
31
54
|
def set_status(self, code: int) -> None:
|
|
32
55
|
"""
|
|
@@ -96,6 +96,12 @@ HttpContext API:
|
|
|
96
96
|
context.set_header(key, value) → Add a custom response header
|
|
97
97
|
context.redirect(url, status=302) → Redirect to another URL
|
|
98
98
|
context.set_binary_response(content, media_type) → Return raw binary data
|
|
99
|
+
context.client_ip → Best-effort caller IP (property, not a
|
|
100
|
+
call) — a raw signal for the PLUGIN's own
|
|
101
|
+
policy (identity-aware rate limiting,
|
|
102
|
+
audit logging...). See INSTRUCTIONS_FOR_AI.md's
|
|
103
|
+
Rate Limiting Pattern: this tool never
|
|
104
|
+
decides a policy, it only exposes the signal.
|
|
99
105
|
|
|
100
106
|
|
|
101
107
|
AUTH VALIDATOR CONTRACT:
|
|
@@ -132,7 +138,9 @@ REPLACEMENT STANDARD (implement this to swap the backend):
|
|
|
132
138
|
add_sse_endpoint(path, generator, tags, auth_validator)
|
|
133
139
|
4. Handler contract: handler(data: dict, context: HttpContext) → dict
|
|
134
140
|
- data: flat merge of path params + query params + body (+ _files if applicable)
|
|
135
|
-
- context: instance of HttpContext (or a compatible duck-type)
|
|
141
|
+
- context: instance of HttpContext (or a compatible duck-type), constructed with
|
|
142
|
+
client_ip= the best-effort caller IP for this backend's own request object
|
|
143
|
+
(see pipeline.py's _extract_client_ip for the header trust order to mirror)
|
|
136
144
|
5. Honor context.status_code and context.binary_content for the HTTP response.
|
|
137
145
|
If the handler never called context.set_status() and the result dict has
|
|
138
146
|
success: False, default to HTTP 400 instead of 200 (context._status_explicit
|
|
@@ -355,6 +363,10 @@ class HttpServerTool(BaseTool):
|
|
|
355
363
|
- context.set_cookie(key, value, max_age=3600, ...): Set secure response cookie.
|
|
356
364
|
- context.set_header(key, value): Add custom response header.
|
|
357
365
|
- context.set_binary_response(content: bytes, media_type: str): Return raw file.
|
|
366
|
+
- context.client_ip: Best-effort caller IP (property). Raw signal only — the
|
|
367
|
+
plugin decides what to do with it (e.g. state.increment() keyed by IP for
|
|
368
|
+
an identity-aware business rule). Never security-authoritative on its own;
|
|
369
|
+
see context.py's client_ip docstring for the trust order and its limits.
|
|
358
370
|
- RESPONSE CONTRACT:
|
|
359
371
|
- Standard: return {"success": bool, "data": ..., "error": ...}
|
|
360
372
|
- WARNING: All values in 'data' must be JSON-serializable. Pydantic model
|