microcoreos 0.2.2__tar.gz → 0.3.1__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.1/.env.example +191 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/.gitignore +7 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/AI_CONTEXT.md +42 -3
- {microcoreos-0.2.2 → microcoreos-0.3.1}/PKG-INFO +2 -1
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_tools/auth/auth_tool.py +22 -3
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_tools/scheduler/scheduler_tool.py +114 -1
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos/catalog.py +44 -2
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos/scaffold.py +57 -1
- {microcoreos-0.2.2 → microcoreos-0.3.1}/pyproject.toml +18 -1
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_auth_tool.py +33 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_catalog.py +122 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_context_tool.py +77 -2
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_core.py +137 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_core_purity.py +1 -1
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_event_bus_tool.py +60 -0
- microcoreos-0.3.1/tests/test_http_pipeline.py +222 -0
- microcoreos-0.3.1/tests/test_http_server_tool.py +527 -0
- microcoreos-0.3.1/tests/test_kernel.py +348 -0
- microcoreos-0.3.1/tests/test_scheduler_tool.py +215 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_sqlite_tool.py +5 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_tool_proxy.py +12 -1
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/tools/test_event_bus_broker_parity.py +1 -1
- microcoreos-0.3.1/tests/tools/test_registry_tool.py +52 -0
- microcoreos-0.3.1/tests/tools/test_telemetry_tool.py +247 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/event_bus/sqlite_driver.py +7 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/http_server/http_server_tool.py +207 -22
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/http_server/pipeline.py +29 -1
- microcoreos-0.3.1/tools/http_server/types.py +100 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/sqlite/sqlite_tool.py +18 -16
- {microcoreos-0.2.2 → microcoreos-0.3.1}/uv.lock +435 -1
- microcoreos-0.2.2/.env.example +0 -96
- microcoreos-0.2.2/tests/test_http_server_tool.py +0 -151
- microcoreos-0.2.2/tests/test_kernel.py +0 -131
- microcoreos-0.2.2/tests/test_scheduler_tool.py +0 -96
- {microcoreos-0.2.2 → microcoreos-0.3.1}/.agent/skills/microcoreos-architecture/SKILL.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/.agent/skills/microcoreos-architecture/agent.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/.agent/workflows/feature-plan.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/.agent/workflows/multi-domain-plan.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/.agent/workflows/new-domain.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/.agent/workflows/new-tool.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/.claude/settings.local.json +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/.dockerignore +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/.github/workflows/ci.yml +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/.github/workflows/release.yml +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/.python-version +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/AGENTS.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/Dockerfile +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/INSTRUCTIONS_FOR_AI.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/LICENSE +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/README.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/ROADMAP.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/cli.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/dev_infra/cache_probe.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/dev_infra/docker-compose.yml +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/docs/CLI.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/docs/CORE_INFRASTRUCTURE.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/docs/ELASTIC_DEPLOYMENT.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/docs/EVENT_BUS.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/docs/HTTP_SERVER.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/docs/INDEX.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/docs/OBSERVABILITY.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/docs/OBSERVABILITY_API.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/docs/PARALLEL_DEVELOPMENT.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/docs/translations/es/README.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/domains/devtools/lint/plugin_sources.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/domains/devtools/plugins/discovery_naming_linter_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/domains/devtools/plugins/domain_isolation_linter_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/domains/devtools/plugins/event_contract_linter_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/domains/devtools/plugins/event_schemas_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/domains/devtools/plugins/field_divergence_linter_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/domains/devtools/plugins/route_collision_linter_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/domains/devtools/plugins/table_ownership_linter_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/domains/devtools/plugins/tool_doc_drift_linter_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/domains/system/plugins/event_delivery_monitor_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/domains/system/plugins/system_events_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/domains/system/plugins/system_events_stream_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/domains/system/plugins/system_logs_stream_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/domains/system/plugins/system_metrics_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/domains/system/plugins/system_status_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/domains/system/plugins/system_traces_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/domains/system/plugins/system_traces_stream_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/domains/system/plugins/tool_health_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_domains/chaos/plugins/blocking_boot_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_domains/chaos/plugins/chaos_control_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_domains/chaos/plugins/failing_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_domains/chaos/plugins/stress_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_domains/ping/plugins/ping_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_domains/scheduler/migrations/001_scheduler_one_shots.sql +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_domains/scheduler/models/scheduler_one_shot.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_domains/scheduler/plugins/durable_one_shots_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_domains/users/migrations/001_create_users.sql +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_domains/users/models/user.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_domains/users/plugins/create_user_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_domains/users/plugins/delete_user_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_domains/users/plugins/get_me_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_domains/users/plugins/get_user_by_id_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_domains/users/plugins/get_users_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_domains/users/plugins/login_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_domains/users/plugins/logout_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_domains/users/plugins/update_user_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_domains/users/plugins/welcome_service_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_tools/chaos/chaos_tool.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_tools/kafka/kafka_driver.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_tools/postgresql/postgresql_tool.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_tools/rabbitmq/rabbitmq_driver.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_tools/redis_state/redis_state_tool.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_tools/s3/__init__.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/extras/available_tools/s3/s3_tool.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/hatch_build.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/main.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos/__init__.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos/base_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos/base_tool.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos/cli.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos/container.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos/context.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos/kernel.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos/project.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos/project_readme.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos/registry.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos/upgrade.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos_dev/__init__.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos_dev/cli.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos_dev/fuzzer.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos_dev/pipeline.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos_dev/plan/__init__.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos_dev/plan/rules.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos_dev/plan/scan.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos_dev/plan/schema.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/microcoreos_dev/probe.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/plans/README.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/plans/active_plan.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/plans/active_plan.yaml +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/conftest.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/dev/corpus/README.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/dev/corpus/qwen_twitter_plan.yaml +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/dev/test_pipeline.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/dev/test_plan_validator.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/domains/ping/test_ping_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/domains/users/test_create_user_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/domains/users/test_delete_user_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/domains/users/test_get_me_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/domains/users/test_get_user_by_id_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/domains/users/test_get_users_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/domains/users/test_login_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/domains/users/test_logout_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/domains/users/test_update_user_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/domains/users/test_welcome_service_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/helpers/active_db.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/helpers/async_wait.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/helpers/mock_db.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/helpers/trace_chains.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_chaos_control.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_chaos_tool.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_cli.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_config_tool.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_discovery_naming_linter.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_domain_isolation_linter.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_durable_one_shots.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_event_bus_groups.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_event_contract_linter.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_event_schemas_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_field_divergence_linter.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_http_params_hardening.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_logger_tool.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_no_retry.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_plugin_di_fixtures.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_postgresql_describe_schema.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_postgresql_tool.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_registry_collisions.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_route_collision_linter.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_s3_tool.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_scaffold.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_scheduler_singleton.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_security_hardening.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_sqlite_concurrency.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_sqlite_describe_schema.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_sqlite_migrations.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_state_tool.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_system_events_stats.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_system_traces_plugin.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_table_ownership_linter.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_tool_doc_drift_linter.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_trace_chain_helper.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/test_upgrade.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/tools/test_db_parity.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/tools/test_event_bus_kafka_parity.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/tools/test_event_bus_rabbitmq_parity.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/tools/test_redis_streams_driver.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/tools/test_s3_parity.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/tools/test_sqlite_driver.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tests/tools/test_state_parity.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/config/config_tool.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/context/authoring_guide.md +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/context/context_tool.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/context/renderers.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/context/scanners.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/event_bus/drivers.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/event_bus/envelope.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/event_bus/event_bus_tool.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/event_bus/redis_streams_driver.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/http_server/context.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/logger/logger_tool.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/sqlite/errors.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/sqlite/migrations.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/sqlite/transaction.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/state/state_tool.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/system/registry_tool.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/telemetry/__init__.py +0 -0
- {microcoreos-0.2.2 → microcoreos-0.3.1}/tools/telemetry/telemetry_tool.py +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# ╔════════════════════════════════════════════════════════════════════════════╗
|
|
2
|
+
# ║ MicroCoreOS — every environment variable, with its default. ║
|
|
3
|
+
# ║ ║
|
|
4
|
+
# ║ This file is the REFERENCE: commented options belong here. ║
|
|
5
|
+
# ║ Your .env is STATE: only settings in effect, none commented out. ║
|
|
6
|
+
# ║ ║
|
|
7
|
+
# ║ `microcoreos add <extra>` appends a section like these to your .env, so ║
|
|
8
|
+
# ║ leaving a variable commented there makes it get appended a second time. ║
|
|
9
|
+
# ╚════════════════════════════════════════════════════════════════════════════╝
|
|
10
|
+
|
|
11
|
+
# ╭────────────────────────────────────────────────────────────────────────────╮
|
|
12
|
+
# │ HTTP SERVER tools/http_server │
|
|
13
|
+
# ╰────────────────────────────────────────────────────────────────────────────╯
|
|
14
|
+
|
|
15
|
+
HTTP_PORT=5000
|
|
16
|
+
|
|
17
|
+
# Bind address. Default: 127.0.0.1 (dev-safe).
|
|
18
|
+
# PRODUCTION / DOCKER: must be 0.0.0.0 or the service refuses external connections.
|
|
19
|
+
# HTTP_HOST=0.0.0.0
|
|
20
|
+
|
|
21
|
+
# CORS allowed origins. Default: * (dev-friendly).
|
|
22
|
+
# Production: your actual frontend domain(s), comma-separated.
|
|
23
|
+
# HTTP_CORS_ORIGINS=https://app.example.com,https://admin.example.com
|
|
24
|
+
|
|
25
|
+
# Let cross-origin requests carry cookies. Default: false. Needed only when a
|
|
26
|
+
# browser app on ANOTHER origin authenticates by cookie instead of by Bearer
|
|
27
|
+
# token. Requires an explicit HTTP_CORS_ORIGINS list — the tool refuses to boot
|
|
28
|
+
# with '*', which would authorize every site to send the session cookie.
|
|
29
|
+
# HTTP_CORS_CREDENTIALS=true
|
|
30
|
+
|
|
31
|
+
# Uvicorn log level. Default: warning. Set to info for HTTP access logs.
|
|
32
|
+
# HTTP_LOG_LEVEL=info
|
|
33
|
+
|
|
34
|
+
# ╭────────────────────────────────────────────────────────────────────────────╮
|
|
35
|
+
# │ DATABASE — SQLite tools/sqlite │
|
|
36
|
+
# ╰────────────────────────────────────────────────────────────────────────────╯
|
|
37
|
+
|
|
38
|
+
SQLITE_DB_PATH=database.db
|
|
39
|
+
|
|
40
|
+
# Migrations policy. Default: true (boot applies pending migrations).
|
|
41
|
+
# PRODUCTION: migrating from a replica is PROHIBITED — set false in EVERY
|
|
42
|
+
# replica. Migrations run ONLY as a CI/CD step, in a SINGLE instance, under
|
|
43
|
+
# explicit human supervision:
|
|
44
|
+
# DB_AUTO_MIGRATE=true uv run main.py --boot-tool db
|
|
45
|
+
# DB_AUTO_MIGRATE=false
|
|
46
|
+
|
|
47
|
+
# ╭────────────────────────────────────────────────────────────────────────────╮
|
|
48
|
+
# │ EVENT BUS tools/event_bus │
|
|
49
|
+
# ╰────────────────────────────────────────────────────────────────────────────╯
|
|
50
|
+
|
|
51
|
+
# Driver. Default: in_process (same-process, zero infrastructure).
|
|
52
|
+
# Options: in_process | sqlite | redis_streams | kafka | rabbitmq
|
|
53
|
+
# The last two need `microcoreos add kafka` / `add rabbitmq` — see their sections.
|
|
54
|
+
# EVENT_BUS_DRIVER=in_process
|
|
55
|
+
|
|
56
|
+
# Dead-letter queue for events that exhausted their retries. Default: true.
|
|
57
|
+
# EVENT_BUS_DLQ_ENABLED=false
|
|
58
|
+
|
|
59
|
+
# --- sqlite driver: durable queue on disk, no broker ---
|
|
60
|
+
# EVENT_BUS_SQLITE_PATH=event_bus_queue.db
|
|
61
|
+
# EVENT_BUS_SQLITE_MAXLEN=10000 # rows kept per stream
|
|
62
|
+
# EVENT_BUS_SQLITE_POLL_MS=25 # consumer poll interval
|
|
63
|
+
# EVENT_BUS_SQLITE_SYNCHRONOUS=FULL # FULL | NORMAL — NORMAL is faster, less durable
|
|
64
|
+
|
|
65
|
+
# --- redis_streams driver: also needs the REDIS section below ---
|
|
66
|
+
# EVENT_BUS_STREAM_MAXLEN=10000 # entries kept per stream
|
|
67
|
+
# EVENT_BUS_CLAIM_IDLE_MS=60000 # ms before a stalled message is reclaimed
|
|
68
|
+
# EVENT_BUS_DELAY_POLL_MS=500 # delayed-delivery poll interval
|
|
69
|
+
|
|
70
|
+
# ╭────────────────────────────────────────────────────────────────────────────╮
|
|
71
|
+
# │ TELEMETRY — OpenTelemetry tools/telemetry │
|
|
72
|
+
# ╰────────────────────────────────────────────────────────────────────────────╯
|
|
73
|
+
|
|
74
|
+
# ALL tool calls get spans automatically via ToolProxy — no plugin changes.
|
|
75
|
+
# Install first:
|
|
76
|
+
# uv add opentelemetry-sdk opentelemetry-exporter-otlp opentelemetry-instrumentation-fastapi
|
|
77
|
+
OTEL_ENABLED=false
|
|
78
|
+
OTEL_SERVICE_NAME=microcoreos
|
|
79
|
+
|
|
80
|
+
# Export destination. Commented = console exporter (dev).
|
|
81
|
+
# Jaeger, Grafana Tempo, or any OTLP/gRPC backend.
|
|
82
|
+
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
|
83
|
+
|
|
84
|
+
# BatchSpanProcessor tuning (OTel standard vars)
|
|
85
|
+
# OTEL_BSP_SCHEDULE_DELAY=5000 # ms between export batches
|
|
86
|
+
# OTEL_BSP_MAX_EXPORT_BATCH_SIZE=512 # spans per batch
|
|
87
|
+
# OTEL_BSP_MAX_QUEUE_SIZE=2048 # in-memory buffer size
|
|
88
|
+
|
|
89
|
+
# ╭────────────────────────────────────────────────────────────────────────────╮
|
|
90
|
+
# │ HEALTH CHECK domains/system │
|
|
91
|
+
# ╰────────────────────────────────────────────────────────────────────────────╯
|
|
92
|
+
|
|
93
|
+
# ToolHealthPlugin calls db.health_check() periodically and updates the registry.
|
|
94
|
+
HEALTH_CHECK_INTERVAL=30
|
|
95
|
+
|
|
96
|
+
# ╭────────────────────────────────────────────────────────────────────────────╮
|
|
97
|
+
# │ METRICS always on │
|
|
98
|
+
# ╰────────────────────────────────────────────────────────────────────────────╯
|
|
99
|
+
|
|
100
|
+
# Timing needs no config — ToolProxy measures every tool call:
|
|
101
|
+
# records = self.registry.get_metrics()
|
|
102
|
+
# self.registry.add_metrics_sink(callback)
|
|
103
|
+
# Each record: {tool, method, duration_ms, success, timestamp}
|
|
104
|
+
|
|
105
|
+
# ╭────────────────────────────────────────────────────────────────────────────╮
|
|
106
|
+
# │ AUTHENTICATION microcoreos add auth │
|
|
107
|
+
# ╰────────────────────────────────────────────────────────────────────────────╯
|
|
108
|
+
|
|
109
|
+
# AUTH_SECRET_KEY=change-me # REQUIRED, 32 chars minimum
|
|
110
|
+
# AUTH_TOKEN_EXPIRE_MINUTES=60
|
|
111
|
+
# AUTH_ALGORITHM=HS256
|
|
112
|
+
|
|
113
|
+
# ╭────────────────────────────────────────────────────────────────────────────╮
|
|
114
|
+
# │ DATABASE — PostgreSQL microcoreos add postgres │
|
|
115
|
+
# ╰────────────────────────────────────────────────────────────────────────────╯
|
|
116
|
+
|
|
117
|
+
# Replaces the sqlite tool: the LAST one to register wins the 'db' key.
|
|
118
|
+
# PG_HOST=localhost
|
|
119
|
+
# PG_PORT=5432
|
|
120
|
+
# PG_USER=postgres
|
|
121
|
+
# PG_PASSWORD=postgres
|
|
122
|
+
# PG_DATABASE=microcoreos
|
|
123
|
+
# PG_MIN_POOL=1
|
|
124
|
+
# PG_MAX_POOL=10
|
|
125
|
+
# PG_CONNECT_TIMEOUT=5
|
|
126
|
+
# PG_COMMAND_TIMEOUT=30
|
|
127
|
+
|
|
128
|
+
# ╭────────────────────────────────────────────────────────────────────────────╮
|
|
129
|
+
# │ REDIS microcoreos add redis │
|
|
130
|
+
# ╰────────────────────────────────────────────────────────────────────────────╯
|
|
131
|
+
|
|
132
|
+
# Swaps the in-memory state tool. For the Redis Streams EVENT BUS instead,
|
|
133
|
+
# set EVENT_BUS_DRIVER=redis_streams — that driver already ships in tools/event_bus/.
|
|
134
|
+
# REDIS_HOST=localhost
|
|
135
|
+
# REDIS_PORT=6379
|
|
136
|
+
# REDIS_DB=0
|
|
137
|
+
# REDIS_PASSWORD= # empty for no auth
|
|
138
|
+
# REDIS_CONNECT_TIMEOUT=5
|
|
139
|
+
|
|
140
|
+
# ╭────────────────────────────────────────────────────────────────────────────╮
|
|
141
|
+
# │ S3 STORAGE microcoreos add s3 │
|
|
142
|
+
# ╰────────────────────────────────────────────────────────────────────────────╯
|
|
143
|
+
|
|
144
|
+
# AWS_ACCESS_KEY_ID=your-access-key
|
|
145
|
+
# AWS_SECRET_ACCESS_KEY=your-secret-key
|
|
146
|
+
# AWS_DEFAULT_REGION=us-east-1
|
|
147
|
+
# AWS_S3_ENDPOINT_URL=http://localhost:9000 # MinIO in dev; drop for real S3
|
|
148
|
+
# AWS_S3_DEFAULT_BUCKET=microcoreos-bucket
|
|
149
|
+
# AWS_S3_SIZE_LIMIT_ENABLED=true
|
|
150
|
+
# AWS_S3_MAX_FILE_SIZE_MB=10
|
|
151
|
+
# AWS_S3_VERIFY_SSL=true
|
|
152
|
+
|
|
153
|
+
# ╭────────────────────────────────────────────────────────────────────────────╮
|
|
154
|
+
# │ SCHEDULER microcoreos add scheduler │
|
|
155
|
+
# ╰────────────────────────────────────────────────────────────────────────────╯
|
|
156
|
+
|
|
157
|
+
# false on worker replicas: jobs register everywhere, fire in ONE beat replica.
|
|
158
|
+
# SCHEDULER_ENABLED=true
|
|
159
|
+
|
|
160
|
+
# ╭────────────────────────────────────────────────────────────────────────────╮
|
|
161
|
+
# │ KAFKA microcoreos add kafka │
|
|
162
|
+
# ╰────────────────────────────────────────────────────────────────────────────╯
|
|
163
|
+
|
|
164
|
+
# EVENT_BUS_DRIVER=kafka
|
|
165
|
+
# KAFKA_BOOTSTRAP_SERVERS=localhost:9092
|
|
166
|
+
# KAFKA_BUS_TOPIC_PREFIX=bus.
|
|
167
|
+
# KAFKA_BUS_PARTITIONS=6
|
|
168
|
+
# KAFKA_BUS_REPLICATION=1
|
|
169
|
+
# KAFKA_CONNECT_TIMEOUT=5
|
|
170
|
+
# KAFKA_MAX_POLL_INTERVAL_MS=300000
|
|
171
|
+
|
|
172
|
+
# ╭────────────────────────────────────────────────────────────────────────────╮
|
|
173
|
+
# │ RABBITMQ microcoreos add rabbitmq │
|
|
174
|
+
# ╰────────────────────────────────────────────────────────────────────────────╯
|
|
175
|
+
|
|
176
|
+
# EVENT_BUS_DRIVER=rabbitmq
|
|
177
|
+
# RABBITMQ_HOST=localhost
|
|
178
|
+
# RABBITMQ_PORT=5672
|
|
179
|
+
# RABBITMQ_USER=guest
|
|
180
|
+
# RABBITMQ_PASSWORD=guest
|
|
181
|
+
# RABBITMQ_VHOST=/
|
|
182
|
+
# RABBITMQ_PREFETCH=16
|
|
183
|
+
# RABBITMQ_CONNECT_TIMEOUT=5
|
|
184
|
+
|
|
185
|
+
# ╭────────────────────────────────────────────────────────────────────────────╮
|
|
186
|
+
# │ CHAOS ENGINEERING microcoreos add chaos │
|
|
187
|
+
# ╰────────────────────────────────────────────────────────────────────────────╯
|
|
188
|
+
|
|
189
|
+
# Intentionally fails tools/plugins on boot to test fault tolerance.
|
|
190
|
+
# NEVER enable in production.
|
|
191
|
+
# CHAOS_ENABLED=true
|
|
@@ -23,7 +23,8 @@ HTTP Server Tool (http):
|
|
|
23
23
|
'data' = flat merge of [path params] + [query params] + [body/form fields].
|
|
24
24
|
Special keys in 'data':
|
|
25
25
|
- data["_auth"]: contains the payload from auth_validator if successful.
|
|
26
|
-
- data["_files"]: list of
|
|
26
|
+
- data["_files"]: list of UploadedFile objects (only if has_files=True).
|
|
27
|
+
Fields: .filename, .content_type, .stream (sync file object), await .read().
|
|
27
28
|
- SECURITY DEFAULTS:
|
|
28
29
|
- Cookies set via context.set_cookie are 'Secure=True', 'HttpOnly=True', 'SameSite=Lax'.
|
|
29
30
|
- CSRF Guard: Mutations (POST/PUT/DELETE) using cookie auth REQUIRE 'X-Requested-With' header.
|
|
@@ -35,8 +36,19 @@ HTTP Server Tool (http):
|
|
|
35
36
|
- has_files: if True, enables multipart/form-data. Request model fields
|
|
36
37
|
become Form fields. To use a file: file = data["_files"][0];
|
|
37
38
|
await s3.upload_fileobj(file.filename, file.file, content_type=file.content_type)
|
|
38
|
-
- mount_static(path, directory_path):
|
|
39
|
-
|
|
39
|
+
- mount_static(path, directory_path, html=False, allow_extensions=None):
|
|
40
|
+
Serve static files from a directory. Deny by default: only files whose
|
|
41
|
+
extension is allowed are served (default DEFAULT_STATIC_EXTENSIONS; pass
|
|
42
|
+
a set to declare your own, or "*" to serve everything). Dotfiles are
|
|
43
|
+
always refused except under '.well-known/'. Use html=True to serve
|
|
44
|
+
index.html for directory requests, which a UI/SPA mounted at "/" needs.
|
|
45
|
+
Raises ValueError if the directory does not exist.
|
|
46
|
+
- add_ws_endpoint(path, on_connect, on_disconnect=None, auth_validator=None):
|
|
47
|
+
WebSocket support. on_connect receives a WebSocketConnection: send_text,
|
|
48
|
+
send_json, receive_text, receive_json, close, query_params, path_params.
|
|
49
|
+
With auth_validator the token is read from the Authorization header, the
|
|
50
|
+
`token` query param, then the access_token cookie; an invalid one is
|
|
51
|
+
closed with 1008 BEFORE the handshake and on_connect takes (conn, payload).
|
|
40
52
|
- add_sse_endpoint(path, generator, tags=None, auth_validator=None):
|
|
41
53
|
Server-Sent Events. generator yields formatted strings: "data: {...}\n\n".
|
|
42
54
|
- register_pre_mount_hook(hook): hook(endpoints: list[dict]) is called once in
|
|
@@ -286,6 +298,33 @@ Async SQLite Persistence Tool (sqlite):
|
|
|
286
298
|
|
|
287
299
|
## 📦 Domains
|
|
288
300
|
|
|
301
|
+
### `chaos`
|
|
302
|
+
- **Tables**: none
|
|
303
|
+
- **Endpoints**:
|
|
304
|
+
- `GET /system/chaos`
|
|
305
|
+
- **res**: ChaosStateData(faults: list[FaultSpecView(tool: str, source: str, mode: str, plugin: Optional[str], rate: Optional[float], seconds: Optional[float])], wrapped_tools: list[str], paused_plugins: list[str])
|
|
306
|
+
- `POST /system/chaos/fail`
|
|
307
|
+
- **req**: plugin: str, tool: Optional[str], rate: float
|
|
308
|
+
- **res**: FailData(plugin: str, tool: Optional[str], rate: float)
|
|
309
|
+
- `POST /system/chaos/latency`
|
|
310
|
+
- **req**: plugin: Optional[str], tool: Optional[str], seconds: float
|
|
311
|
+
- **res**: LatencyData(plugin: Optional[str], tool: Optional[str], seconds: float)
|
|
312
|
+
- `POST /system/chaos/off`
|
|
313
|
+
- **req**: plugin: str
|
|
314
|
+
- **res**: PluginPauseData(plugin: str, paused: bool)
|
|
315
|
+
- `POST /system/chaos/on`
|
|
316
|
+
- **req**: plugin: str
|
|
317
|
+
- **res**: PluginPauseData(plugin: str, paused: bool)
|
|
318
|
+
- `POST /system/chaos/reset`
|
|
319
|
+
- **res**: ResetData(cleared: int, restored_tools: list[str], resumed_plugins: list[str])
|
|
320
|
+
- `POST /system/chaos/tool`
|
|
321
|
+
- **req**: name: str, mode: Literal['down', 'slow', 'flaky', 'off'], seconds: float, rate: float
|
|
322
|
+
- **res**: ToolFaultData(name: str, mode: str)
|
|
323
|
+
- **Events emitted**: `system.chaos.fail_armed` (plugin, rate, tool), `system.chaos.latency_armed` (plugin, seconds, tool), `system.chaos.plugin_paused` (plugin), `system.chaos.plugin_resumed` (plugin), `system.chaos.reset` (cleared), `system.chaos.tool_fault_armed` (mode, tool)
|
|
324
|
+
- **Events consumed**: none
|
|
325
|
+
- **Dependencies**: container, event_bus, http, logger
|
|
326
|
+
- **Plugins**: chaos.ChaosControlPlugin
|
|
327
|
+
|
|
289
328
|
### `devtools`
|
|
290
329
|
- **Tables**: none
|
|
291
330
|
- **Endpoints**:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microcoreos
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Atomic Microkernel Architecture optimized for AI-Driven Development
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -9,6 +9,7 @@ Requires-Dist: aiosqlite>=0.20.0
|
|
|
9
9
|
Requires-Dist: fastapi>=0.115.0
|
|
10
10
|
Requires-Dist: pydantic[email]>=2.10.0
|
|
11
11
|
Requires-Dist: python-dotenv>=1.0.0
|
|
12
|
+
Requires-Dist: python-multipart>=0.0.9
|
|
12
13
|
Requires-Dist: pyyaml>=6.0.3
|
|
13
14
|
Requires-Dist: uvicorn>=0.30.0
|
|
14
15
|
Requires-Dist: websockets>=13.0
|
|
@@ -28,6 +28,8 @@ REPLACEMENT STANDARD (plugins unaffected):
|
|
|
28
28
|
from decode_token. validate_token returns None instead of raising.
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
|
+
import base64
|
|
32
|
+
import hashlib
|
|
31
33
|
import os
|
|
32
34
|
import asyncio
|
|
33
35
|
import bcrypt
|
|
@@ -37,6 +39,20 @@ from typing import Optional
|
|
|
37
39
|
from microcoreos import BaseTool
|
|
38
40
|
|
|
39
41
|
|
|
42
|
+
def _prehash(password: str) -> bytes:
|
|
43
|
+
"""
|
|
44
|
+
bcrypt hashes at most 72 bytes and stops at the first NUL, both silently:
|
|
45
|
+
two passwords sharing a 72-byte prefix verify against the same hash.
|
|
46
|
+
Hashing first makes every input a fixed 44-byte, NUL-free digest, so the
|
|
47
|
+
whole password decides the result. base64 rather than hex to stay well
|
|
48
|
+
under the limit. This is passlib's bcrypt_sha256 construction.
|
|
49
|
+
|
|
50
|
+
Changing this function invalidates every stored hash — they can only be
|
|
51
|
+
reissued by a password reset, since the plaintext is not recoverable.
|
|
52
|
+
"""
|
|
53
|
+
return base64.b64encode(hashlib.sha256(password.encode("utf-8")).digest())
|
|
54
|
+
|
|
55
|
+
|
|
40
56
|
class AuthError(Exception):
|
|
41
57
|
"""Base class for authentication failures."""
|
|
42
58
|
|
|
@@ -74,9 +90,12 @@ class AuthTool(BaseTool):
|
|
|
74
90
|
- PURPOSE: Manage system security, password hashing, and JWT token lifecycle.
|
|
75
91
|
- CAPABILITIES:
|
|
76
92
|
- await hash_password(password: str) -> str: Securely hashes a plain-text
|
|
77
|
-
password
|
|
93
|
+
password: SHA-256 first, then bcrypt, so the whole password counts
|
|
94
|
+
regardless of length (bare bcrypt silently ignores everything past
|
|
95
|
+
72 bytes). Async — runs in a thread (bcrypt is CPU-bound).
|
|
78
96
|
- await verify_password(password: str, hashed_password: str) -> bool:
|
|
79
97
|
Verifies if a password matches its hash. Async — runs in a thread.
|
|
98
|
+
Only hashes produced by this tool's hash_password() verify.
|
|
80
99
|
- create_token(data: dict, expires_delta: Optional[int] = None) -> str:
|
|
81
100
|
Generates a JWT signed token. 'data' should contain claims (e.g. {'sub': user_id}).
|
|
82
101
|
'expires_delta' is optional minutes until expiration.
|
|
@@ -92,12 +111,12 @@ class AuthTool(BaseTool):
|
|
|
92
111
|
# bcrypt is CPU-bound (~100ms by design) — run in a thread so it
|
|
93
112
|
# never blocks the event loop under concurrent requests.
|
|
94
113
|
return await asyncio.to_thread(
|
|
95
|
-
lambda: bcrypt.hashpw(password
|
|
114
|
+
lambda: bcrypt.hashpw(_prehash(password), bcrypt.gensalt()).decode()
|
|
96
115
|
)
|
|
97
116
|
|
|
98
117
|
async def verify_password(self, password: str, hashed_password: str) -> bool:
|
|
99
118
|
return await asyncio.to_thread(
|
|
100
|
-
bcrypt.checkpw, password
|
|
119
|
+
bcrypt.checkpw, _prehash(password), hashed_password.encode()
|
|
101
120
|
)
|
|
102
121
|
|
|
103
122
|
def create_token(self, data: dict, expires_delta: Optional[int] = None) -> str:
|
|
@@ -12,6 +12,10 @@ PUBLIC CONTRACT (what plugins use):
|
|
|
12
12
|
job_id = scheduler.add_job("0 * * * *", self.on_every_hour)
|
|
13
13
|
job_id = scheduler.add_job("*/5 * * * *", self.send_digest, job_id="digest")
|
|
14
14
|
|
|
15
|
+
# Recurring job — fixed interval, for the sub-minute rates cron cannot express
|
|
16
|
+
job_id = scheduler.add_interval_job(1.0, self.sample_metrics)
|
|
17
|
+
job_id = scheduler.add_interval_job(0.25, self.poll, job_id="poll", max_instances=4)
|
|
18
|
+
|
|
15
19
|
# One-shot job — runs once at a specific datetime
|
|
16
20
|
from datetime import datetime, timedelta, timezone
|
|
17
21
|
run_at = datetime.now(timezone.utc) + timedelta(minutes=30)
|
|
@@ -86,13 +90,23 @@ REPLACEMENT STANDARD (swap without changing plugins):
|
|
|
86
90
|
To replace with Celery beat or any other scheduler:
|
|
87
91
|
1. Create tools/{name}/{name}_tool.py
|
|
88
92
|
2. Set name = "scheduler" ← same injection key
|
|
89
|
-
3. Implement the
|
|
93
|
+
3. Implement the 5 public methods:
|
|
90
94
|
add_job(cron_expr, callback, job_id?) → str
|
|
95
|
+
add_interval_job(seconds, callback, job_id?, ...) → str
|
|
91
96
|
add_one_shot(run_at, callback, job_id?) → str
|
|
92
97
|
remove_job(job_id) → bool
|
|
93
98
|
list_jobs() → list[dict]
|
|
94
99
|
4. Honor SCHEDULER_ENABLED (jobs register everywhere, fire in one place).
|
|
95
100
|
Plugins do not change.
|
|
101
|
+
|
|
102
|
+
LIMIT OF THIS CONTRACT: every method above takes a Python callable, so it
|
|
103
|
+
is implementable only by a scheduler running IN THIS PROCESS (APScheduler,
|
|
104
|
+
`schedule`, a bare asyncio loop). Celery beat and every other distributed
|
|
105
|
+
beat dispatch task NAMES to a broker for other processes to run, and a
|
|
106
|
+
bound method does not cross a process boundary. Distributed scheduling is
|
|
107
|
+
reached the other way — by scheduling an EVENT rather than a function:
|
|
108
|
+
"scheduler.one_shot.schedule" on the bus, where everything crossing the
|
|
109
|
+
boundary is JSON. See durable_one_shots_plugin.py.
|
|
96
110
|
"""
|
|
97
111
|
|
|
98
112
|
import functools
|
|
@@ -170,7 +184,11 @@ class SchedulerTool(BaseTool):
|
|
|
170
184
|
def setup(self) -> None:
|
|
171
185
|
try:
|
|
172
186
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
187
|
+
from apscheduler.events import EVENT_JOB_MISSED, EVENT_JOB_MAX_INSTANCES
|
|
173
188
|
self._scheduler = AsyncIOScheduler()
|
|
189
|
+
self._scheduler.add_listener(
|
|
190
|
+
self._on_run_dropped, EVENT_JOB_MISSED | EVENT_JOB_MAX_INSTANCES
|
|
191
|
+
)
|
|
174
192
|
print("[Scheduler] APScheduler initialized.")
|
|
175
193
|
except ImportError:
|
|
176
194
|
raise RuntimeError(
|
|
@@ -188,6 +206,23 @@ class SchedulerTool(BaseTool):
|
|
|
188
206
|
self._scheduler.start()
|
|
189
207
|
print(f"[Scheduler] Started — {job_count} job(s) registered.")
|
|
190
208
|
|
|
209
|
+
def _on_run_dropped(self, event) -> None:
|
|
210
|
+
"""
|
|
211
|
+
APScheduler discards runs without raising: one past its misfire grace
|
|
212
|
+
time (EVENT_JOB_MISSED) or one that would exceed max_instances
|
|
213
|
+
(EVENT_JOB_MAX_INSTANCES) never reaches the callback and returns no
|
|
214
|
+
error to anybody. Unlogged, a job firing less often than it was
|
|
215
|
+
configured to is indistinguishable from one firing correctly.
|
|
216
|
+
"""
|
|
217
|
+
from apscheduler.events import EVENT_JOB_MAX_INSTANCES
|
|
218
|
+
|
|
219
|
+
reason = ("overlapped a still-running execution (max_instances)"
|
|
220
|
+
if event.code == EVENT_JOB_MAX_INSTANCES
|
|
221
|
+
else "was later than its misfire_grace_time")
|
|
222
|
+
# JobExecutionEvent carries scheduled_run_time; JobSubmissionEvent, a list.
|
|
223
|
+
when = getattr(event, "scheduled_run_time", None) or getattr(event, "scheduled_run_times", None)
|
|
224
|
+
print(f"[Scheduler] Run DROPPED — id={event.job_id!r} {reason}, scheduled for {when}")
|
|
225
|
+
|
|
191
226
|
def shutdown(self) -> None:
|
|
192
227
|
if self._scheduler and self._scheduler.running:
|
|
193
228
|
self._scheduler.shutdown(wait=False)
|
|
@@ -229,6 +264,75 @@ class SchedulerTool(BaseTool):
|
|
|
229
264
|
print(f"[Scheduler] Job registered — id={job_id!r} cron={cron_expr!r}")
|
|
230
265
|
return job_id
|
|
231
266
|
|
|
267
|
+
def add_interval_job(
|
|
268
|
+
self,
|
|
269
|
+
seconds: float,
|
|
270
|
+
callback: Callable,
|
|
271
|
+
job_id: Optional[str] = None,
|
|
272
|
+
*,
|
|
273
|
+
minutes: float = 0,
|
|
274
|
+
hours: float = 0,
|
|
275
|
+
max_instances: int = 1,
|
|
276
|
+
coalesce: bool = True,
|
|
277
|
+
misfire_grace_time: Optional[int] = 1,
|
|
278
|
+
) -> str:
|
|
279
|
+
"""
|
|
280
|
+
Schedule a recurring job on a fixed interval.
|
|
281
|
+
|
|
282
|
+
Cron's smallest unit is the minute, so anything faster — and any rate
|
|
283
|
+
that is not a whole number of minutes — has to be an interval.
|
|
284
|
+
|
|
285
|
+
Parameters:
|
|
286
|
+
seconds: Interval in seconds; accepts fractions (0.25 = 4x/second).
|
|
287
|
+
Combined additively with minutes and hours.
|
|
288
|
+
callback: Sync or async callable. Called with no arguments.
|
|
289
|
+
job_id: Optional stable ID. Auto-generated if omitted.
|
|
290
|
+
|
|
291
|
+
The last three mirror APScheduler's job defaults and matter as the
|
|
292
|
+
interval approaches the callback's own duration:
|
|
293
|
+
|
|
294
|
+
max_instances: concurrent runs allowed. At 1, a run starting
|
|
295
|
+
while the previous one is still going is
|
|
296
|
+
DROPPED, not queued.
|
|
297
|
+
coalesce: collapse several missed runs into one.
|
|
298
|
+
misfire_grace_time: seconds late a run may start; past that it is
|
|
299
|
+
DROPPED. None means run it however late.
|
|
300
|
+
|
|
301
|
+
Dropped runs raise nothing — they are reported by _on_run_dropped().
|
|
302
|
+
A job that must not skip needs max_instances above 1, a callback
|
|
303
|
+
faster than the interval, or both.
|
|
304
|
+
|
|
305
|
+
Returns: the job_id string.
|
|
306
|
+
|
|
307
|
+
Examples:
|
|
308
|
+
scheduler.add_interval_job(1.0, self.sample_metrics)
|
|
309
|
+
scheduler.add_interval_job(0.25, self.poll, job_id="poll", max_instances=4)
|
|
310
|
+
scheduler.add_interval_job(0, self.hourly, minutes=90)
|
|
311
|
+
"""
|
|
312
|
+
from apscheduler.triggers.interval import IntervalTrigger
|
|
313
|
+
|
|
314
|
+
if seconds <= 0 and minutes <= 0 and hours <= 0:
|
|
315
|
+
raise ValueError(
|
|
316
|
+
"add_interval_job: interval must be positive — "
|
|
317
|
+
f"got seconds={seconds}, minutes={minutes}, hours={hours}"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
job_id = job_id or uuid.uuid4().hex
|
|
321
|
+
self._scheduler.add_job(
|
|
322
|
+
_with_identity(callback),
|
|
323
|
+
trigger=IntervalTrigger(seconds=seconds, minutes=minutes, hours=hours),
|
|
324
|
+
id=job_id,
|
|
325
|
+
replace_existing=True,
|
|
326
|
+
max_instances=max_instances,
|
|
327
|
+
coalesce=coalesce,
|
|
328
|
+
misfire_grace_time=misfire_grace_time,
|
|
329
|
+
)
|
|
330
|
+
print(
|
|
331
|
+
f"[Scheduler] Interval job registered — id={job_id!r} "
|
|
332
|
+
f"every {seconds}s+{minutes}m+{hours}h max_instances={max_instances}"
|
|
333
|
+
)
|
|
334
|
+
return job_id
|
|
335
|
+
|
|
232
336
|
def add_one_shot(
|
|
233
337
|
self,
|
|
234
338
|
run_at: datetime,
|
|
@@ -307,6 +411,15 @@ class SchedulerTool(BaseTool):
|
|
|
307
411
|
e.g. "*/5 * * * *" = every 5 min, "0 9 * * 1-5" = weekdays at 09:00.
|
|
308
412
|
Returns job_id (auto-generated if not provided).
|
|
309
413
|
Providing a stable job_id prevents duplicates on restart.
|
|
414
|
+
- add_interval_job(seconds: float, callback, job_id?: str, *, minutes, hours,
|
|
415
|
+
max_instances=1, coalesce=True, misfire_grace_time=1) -> str:
|
|
416
|
+
Schedule a recurring job on a fixed interval. Use this for sub-minute
|
|
417
|
+
rates, which a 5-field cron expression cannot express (its unit is the
|
|
418
|
+
minute). seconds accepts fractions: 0.25 = 4x/second.
|
|
419
|
+
At max_instances=1 a run that overlaps the previous one is DROPPED, and
|
|
420
|
+
a run later than misfire_grace_time is DROPPED — silently, as far as the
|
|
421
|
+
callback is concerned. Both are logged as "Run DROPPED". Raise
|
|
422
|
+
max_instances if the job must not skip.
|
|
310
423
|
- add_one_shot(run_at: datetime, callback, job_id?: str) -> str:
|
|
311
424
|
Schedule a one-time job at a specific datetime (timezone-aware).
|
|
312
425
|
Returns job_id. IN-MEMORY: lost if the process restarts before firing.
|
|
@@ -174,12 +174,48 @@ def _install_dependency(extra: str, root: str) -> bool:
|
|
|
174
174
|
return True
|
|
175
175
|
|
|
176
176
|
|
|
177
|
+
ENV_BOX_WIDTH = 76
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _env_section_header(title: str, source: str) -> list:
|
|
181
|
+
"""
|
|
182
|
+
One boxed heading per tool, so .env stays readable as extras accumulate.
|
|
183
|
+
.env.example uses this same function; keep the three lines equal in width
|
|
184
|
+
or the boxes stop lining up.
|
|
185
|
+
"""
|
|
186
|
+
left = f" {title}"
|
|
187
|
+
pad = ENV_BOX_WIDTH - len(left) - len(source) - 2
|
|
188
|
+
return [
|
|
189
|
+
f"# ╭{'─' * ENV_BOX_WIDTH}╮",
|
|
190
|
+
f"# │{left}{' ' * max(pad, 1)}{source} │",
|
|
191
|
+
f"# ╰{'─' * ENV_BOX_WIDTH}╯",
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _env_state(existing: str, var: str) -> str:
|
|
196
|
+
"""Whether var is absent, set, or present but commented out."""
|
|
197
|
+
for line in existing.splitlines():
|
|
198
|
+
stripped = line.strip()
|
|
199
|
+
if stripped.startswith(f"{var}="):
|
|
200
|
+
return "set"
|
|
201
|
+
if stripped.startswith("#") and stripped.lstrip("#").strip().startswith(f"{var}="):
|
|
202
|
+
return "commented"
|
|
203
|
+
return "absent"
|
|
204
|
+
|
|
205
|
+
|
|
177
206
|
def _append_env(root: str, name: str, entries) -> None:
|
|
178
207
|
"""
|
|
179
208
|
Append the extra's settings to .env, once.
|
|
180
209
|
|
|
181
210
|
Anything already defined there is the user's decision and is left alone —
|
|
182
211
|
re-running `add` must never rewrite a password someone typed.
|
|
212
|
+
|
|
213
|
+
A commented-out setting counts as already there. It is not appended again:
|
|
214
|
+
python-dotenv gives precedence to the LAST occurrence, so the duplicate
|
|
215
|
+
would override the line above it and editing that line would do nothing.
|
|
216
|
+
Which of the two the user meant is not knowable here — commenting a
|
|
217
|
+
variable reads equally as "I want the default" and as "I will fill this in
|
|
218
|
+
later" — so the choice is reported rather than made.
|
|
183
219
|
"""
|
|
184
220
|
if not entries:
|
|
185
221
|
return
|
|
@@ -189,12 +225,18 @@ def _append_env(root: str, name: str, entries) -> None:
|
|
|
189
225
|
if os.path.exists(path):
|
|
190
226
|
existing = open(path, encoding="utf-8").read()
|
|
191
227
|
|
|
192
|
-
|
|
228
|
+
states = {e[0]: _env_state(existing, e[0]) for e in entries}
|
|
229
|
+
missing = [e for e in entries if states[e[0]] == "absent"]
|
|
230
|
+
commented = [v for v, s in states.items() if s == "commented"]
|
|
231
|
+
|
|
232
|
+
for var in commented:
|
|
233
|
+
print(f" ! .env has {var} commented out — uncomment it or delete the line.")
|
|
234
|
+
|
|
193
235
|
if not missing:
|
|
194
236
|
print(" ✓ .env already has these settings — unchanged.")
|
|
195
237
|
return
|
|
196
238
|
|
|
197
|
-
block = [
|
|
239
|
+
block = [""] + _env_section_header(name.upper(), f"microcoreos add {name}") + [""]
|
|
198
240
|
for var, value, comment in missing:
|
|
199
241
|
block.append(f"{var}={value}" + (f" # {comment}" if comment else ""))
|
|
200
242
|
|
|
@@ -376,6 +376,62 @@ def _install_test_deps(root: str) -> bool:
|
|
|
376
376
|
return True
|
|
377
377
|
|
|
378
378
|
|
|
379
|
+
def _write_initial_env(example_path: str, env_path: str) -> None:
|
|
380
|
+
"""
|
|
381
|
+
Initializes .env from .env.example.
|
|
382
|
+
|
|
383
|
+
`.env.example` is the reference containing all commented options for extras.
|
|
384
|
+
`.env` is active state: only settings in effect, omitting commented-out
|
|
385
|
+
optional extra settings so that `microcoreos add <extra>` can append them cleanly.
|
|
386
|
+
"""
|
|
387
|
+
from microcoreos.catalog import CATALOG
|
|
388
|
+
|
|
389
|
+
extra_vars = {
|
|
390
|
+
var for extra in CATALOG.values() if extra.env for var, _, _ in extra.env
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
with open(example_path, encoding="utf-8") as f:
|
|
394
|
+
lines = f.readlines()
|
|
395
|
+
|
|
396
|
+
out_lines = []
|
|
397
|
+
skip_section = False
|
|
398
|
+
|
|
399
|
+
for line in lines:
|
|
400
|
+
stripped = line.strip()
|
|
401
|
+
|
|
402
|
+
# Omit commented-out settings for optional extras
|
|
403
|
+
if stripped.startswith("#") and any(
|
|
404
|
+
stripped.lstrip("#").strip().startswith(f"{var}=") for var in extra_vars
|
|
405
|
+
):
|
|
406
|
+
continue
|
|
407
|
+
|
|
408
|
+
# Omit section header boxes for optional extras
|
|
409
|
+
if stripped.startswith("# │") and "microcoreos add" in stripped:
|
|
410
|
+
skip_section = True
|
|
411
|
+
if out_lines and "# ╭──" in out_lines[-1]:
|
|
412
|
+
out_lines.pop()
|
|
413
|
+
continue
|
|
414
|
+
|
|
415
|
+
if skip_section and ("# ╰──" in stripped or "# │" in stripped):
|
|
416
|
+
if "# ╰──" in stripped:
|
|
417
|
+
skip_section = False
|
|
418
|
+
continue
|
|
419
|
+
|
|
420
|
+
out_lines.append(line)
|
|
421
|
+
|
|
422
|
+
cleaned = []
|
|
423
|
+
prev_blank = False
|
|
424
|
+
for line_item in out_lines:
|
|
425
|
+
is_blank = not line_item.strip()
|
|
426
|
+
if is_blank and prev_blank:
|
|
427
|
+
continue
|
|
428
|
+
cleaned.append(line_item)
|
|
429
|
+
prev_blank = is_blank
|
|
430
|
+
|
|
431
|
+
with open(env_path, "w", encoding="utf-8") as f:
|
|
432
|
+
f.writelines(cleaned)
|
|
433
|
+
|
|
434
|
+
|
|
379
435
|
def new(argv: list[str]) -> int:
|
|
380
436
|
"""`microcoreos new <path> [--force] [--no-ai-kit] [--no-install]`"""
|
|
381
437
|
force = "--force" in argv
|
|
@@ -405,7 +461,7 @@ def new(argv: list[str]) -> int:
|
|
|
405
461
|
# .env is configuration, not source: never clobber one that exists.
|
|
406
462
|
env, example = os.path.join(target, ".env"), os.path.join(target, ".env.example")
|
|
407
463
|
if os.path.exists(example) and not os.path.exists(env):
|
|
408
|
-
|
|
464
|
+
_write_initial_env(example, env)
|
|
409
465
|
|
|
410
466
|
name = os.path.basename(target).replace("_", "-").lower() or "my-app"
|
|
411
467
|
|