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.
Files changed (210) hide show
  1. {microcoreos-0.3.2 → microcoreos-0.4.0}/AI_CONTEXT.md +4 -0
  2. {microcoreos-0.3.2 → microcoreos-0.4.0}/PKG-INFO +4 -4
  3. {microcoreos-0.3.2 → microcoreos-0.4.0}/README.md +3 -3
  4. microcoreos-0.4.0/extras/available_domains/users/migrations/001_create_users.sql +11 -0
  5. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/postgresql/postgresql_tool.py +43 -7
  6. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/postgresql/tests/postgresql_describe_schema_test.py +25 -6
  7. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/postgresql/tests/postgresql_tool_test.py +21 -2
  8. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/scaffold.py +74 -0
  9. {microcoreos-0.3.2 → microcoreos-0.4.0}/pyproject.toml +1 -1
  10. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/helpers/active_db.py +18 -2
  11. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/db/test_db_parity.py +52 -13
  12. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/http_server/test_http_pipeline.py +57 -0
  13. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/http_server/context.py +24 -1
  14. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/http_server/http_server_tool.py +13 -1
  15. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/http_server/pipeline.py +39 -1
  16. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/sqlite/migrations.py +22 -5
  17. {microcoreos-0.3.2 → microcoreos-0.4.0}/uv.lock +1 -1
  18. microcoreos-0.3.2/extras/available_domains/users/migrations/001_create_users.sql +0 -12
  19. {microcoreos-0.3.2 → microcoreos-0.4.0}/.agent/skills/microcoreos-architecture/SKILL.md +0 -0
  20. {microcoreos-0.3.2 → microcoreos-0.4.0}/.agent/skills/microcoreos-architecture/agent.md +0 -0
  21. {microcoreos-0.3.2 → microcoreos-0.4.0}/.agent/workflows/feature-plan.md +0 -0
  22. {microcoreos-0.3.2 → microcoreos-0.4.0}/.agent/workflows/multi-domain-plan.md +0 -0
  23. {microcoreos-0.3.2 → microcoreos-0.4.0}/.agent/workflows/new-domain.md +0 -0
  24. {microcoreos-0.3.2 → microcoreos-0.4.0}/.agent/workflows/new-tool.md +0 -0
  25. {microcoreos-0.3.2 → microcoreos-0.4.0}/.claude/settings.local.json +0 -0
  26. {microcoreos-0.3.2 → microcoreos-0.4.0}/.dockerignore +0 -0
  27. {microcoreos-0.3.2 → microcoreos-0.4.0}/.env.example +0 -0
  28. {microcoreos-0.3.2 → microcoreos-0.4.0}/.github/workflows/ci.yml +0 -0
  29. {microcoreos-0.3.2 → microcoreos-0.4.0}/.github/workflows/release.yml +0 -0
  30. {microcoreos-0.3.2 → microcoreos-0.4.0}/.gitignore +0 -0
  31. {microcoreos-0.3.2 → microcoreos-0.4.0}/.mutmut-cache +0 -0
  32. {microcoreos-0.3.2 → microcoreos-0.4.0}/.python-version +0 -0
  33. {microcoreos-0.3.2 → microcoreos-0.4.0}/AGENTS.md +0 -0
  34. {microcoreos-0.3.2 → microcoreos-0.4.0}/Dockerfile +0 -0
  35. {microcoreos-0.3.2 → microcoreos-0.4.0}/INSTRUCTIONS_FOR_AI.md +0 -0
  36. {microcoreos-0.3.2 → microcoreos-0.4.0}/LICENSE +0 -0
  37. {microcoreos-0.3.2 → microcoreos-0.4.0}/ROADMAP.md +0 -0
  38. {microcoreos-0.3.2 → microcoreos-0.4.0}/cli.py +0 -0
  39. {microcoreos-0.3.2 → microcoreos-0.4.0}/conftest.py +0 -0
  40. {microcoreos-0.3.2 → microcoreos-0.4.0}/dev_infra/cache_probe.py +0 -0
  41. {microcoreos-0.3.2 → microcoreos-0.4.0}/dev_infra/docker-compose.yml +0 -0
  42. {microcoreos-0.3.2 → microcoreos-0.4.0}/docs/CLI.md +0 -0
  43. {microcoreos-0.3.2 → microcoreos-0.4.0}/docs/CORE_INFRASTRUCTURE.md +0 -0
  44. {microcoreos-0.3.2 → microcoreos-0.4.0}/docs/ELASTIC_DEPLOYMENT.md +0 -0
  45. {microcoreos-0.3.2 → microcoreos-0.4.0}/docs/EVENT_BUS.md +0 -0
  46. {microcoreos-0.3.2 → microcoreos-0.4.0}/docs/HTTP_SERVER.md +0 -0
  47. {microcoreos-0.3.2 → microcoreos-0.4.0}/docs/INDEX.md +0 -0
  48. {microcoreos-0.3.2 → microcoreos-0.4.0}/docs/OBSERVABILITY.md +0 -0
  49. {microcoreos-0.3.2 → microcoreos-0.4.0}/docs/OBSERVABILITY_API.md +0 -0
  50. {microcoreos-0.3.2 → microcoreos-0.4.0}/docs/PARALLEL_DEVELOPMENT.md +0 -0
  51. {microcoreos-0.3.2 → microcoreos-0.4.0}/docs/translations/es/README.md +0 -0
  52. {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/devtools/lint/plugin_sources.py +0 -0
  53. {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/devtools/plugins/discovery_naming_linter_plugin.py +0 -0
  54. {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/devtools/plugins/doc_path_linter_plugin.py +0 -0
  55. {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/devtools/plugins/domain_isolation_linter_plugin.py +0 -0
  56. {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/devtools/plugins/event_contract_linter_plugin.py +0 -0
  57. {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/devtools/plugins/event_schemas_plugin.py +0 -0
  58. {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/devtools/plugins/field_divergence_linter_plugin.py +0 -0
  59. {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/devtools/plugins/route_collision_linter_plugin.py +0 -0
  60. {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/devtools/plugins/table_ownership_linter_plugin.py +0 -0
  61. {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/devtools/plugins/tool_doc_drift_linter_plugin.py +0 -0
  62. {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/system/plugins/event_delivery_monitor_plugin.py +0 -0
  63. {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/system/plugins/system_events_plugin.py +0 -0
  64. {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/system/plugins/system_events_stream_plugin.py +0 -0
  65. {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/system/plugins/system_logs_stream_plugin.py +0 -0
  66. {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/system/plugins/system_metrics_plugin.py +0 -0
  67. {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/system/plugins/system_status_plugin.py +0 -0
  68. {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/system/plugins/system_traces_plugin.py +0 -0
  69. {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/system/plugins/system_traces_stream_plugin.py +0 -0
  70. {microcoreos-0.3.2 → microcoreos-0.4.0}/domains/system/plugins/tool_health_plugin.py +0 -0
  71. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/chaos/plugins/blocking_boot_plugin.py +0 -0
  72. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/chaos/plugins/chaos_control_plugin.py +0 -0
  73. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/chaos/plugins/failing_plugin.py +0 -0
  74. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/chaos/plugins/stress_plugin.py +0 -0
  75. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/ping/plugins/ping_plugin.py +0 -0
  76. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/scheduler/migrations/001_scheduler_one_shots.sql +0 -0
  77. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/scheduler/models/scheduler_one_shot.py +0 -0
  78. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/scheduler/plugins/durable_one_shots_plugin.py +0 -0
  79. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/users/models/user.py +0 -0
  80. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/users/plugins/create_user_plugin.py +0 -0
  81. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/users/plugins/delete_user_plugin.py +0 -0
  82. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/users/plugins/get_me_plugin.py +0 -0
  83. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/users/plugins/get_user_by_id_plugin.py +0 -0
  84. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/users/plugins/get_users_plugin.py +0 -0
  85. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/users/plugins/login_plugin.py +0 -0
  86. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/users/plugins/logout_plugin.py +0 -0
  87. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/users/plugins/update_user_plugin.py +0 -0
  88. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_domains/users/plugins/welcome_service_plugin.py +0 -0
  89. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/auth/auth_tool.py +0 -0
  90. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/auth/tests/auth_tool_test.py +0 -0
  91. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/chaos/chaos_tool.py +0 -0
  92. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/kafka/kafka_driver.py +0 -0
  93. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/rabbitmq/rabbitmq_driver.py +0 -0
  94. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/redis_state/redis_state_tool.py +0 -0
  95. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/s3/__init__.py +0 -0
  96. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/s3/s3_tool.py +0 -0
  97. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/s3/tests/s3_tool_test.py +0 -0
  98. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/scheduler/scheduler_tool.py +0 -0
  99. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/scheduler/tests/scheduler_singleton_test.py +0 -0
  100. {microcoreos-0.3.2 → microcoreos-0.4.0}/extras/available_tools/scheduler/tests/scheduler_tool_test.py +0 -0
  101. {microcoreos-0.3.2 → microcoreos-0.4.0}/hatch_build.py +0 -0
  102. {microcoreos-0.3.2 → microcoreos-0.4.0}/main.py +0 -0
  103. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/__init__.py +0 -0
  104. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/base_plugin.py +0 -0
  105. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/base_tool.py +0 -0
  106. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/catalog.py +0 -0
  107. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/cli.py +0 -0
  108. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/container.py +0 -0
  109. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/context.py +0 -0
  110. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/kernel.py +0 -0
  111. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/project.py +0 -0
  112. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/project_readme.md +0 -0
  113. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/registry.py +0 -0
  114. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos/upgrade.py +0 -0
  115. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos_dev/__init__.py +0 -0
  116. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos_dev/cli.py +0 -0
  117. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos_dev/fuzzer.py +0 -0
  118. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos_dev/pipeline.py +0 -0
  119. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos_dev/plan/__init__.py +0 -0
  120. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos_dev/plan/rules.py +0 -0
  121. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos_dev/plan/scan.py +0 -0
  122. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos_dev/plan/schema.py +0 -0
  123. {microcoreos-0.3.2 → microcoreos-0.4.0}/microcoreos_dev/probe.py +0 -0
  124. {microcoreos-0.3.2 → microcoreos-0.4.0}/plans/README.md +0 -0
  125. {microcoreos-0.3.2 → microcoreos-0.4.0}/plans/active_plan.md +0 -0
  126. {microcoreos-0.3.2 → microcoreos-0.4.0}/plans/active_plan.yaml +0 -0
  127. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/core/test_core.py +0 -0
  128. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/core/test_core_purity.py +0 -0
  129. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/core/test_kernel.py +0 -0
  130. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/core/test_no_retry.py +0 -0
  131. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/core/test_plugin_di_fixtures.py +0 -0
  132. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/core/test_registry_collisions.py +0 -0
  133. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/core/test_tool_proxy.py +0 -0
  134. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/dev/corpus/README.md +0 -0
  135. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/dev/corpus/qwen_twitter_plan.yaml +0 -0
  136. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/dev/test_pipeline.py +0 -0
  137. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/dev/test_plan_validator.py +0 -0
  138. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/domains/ping/test_ping_plugin.py +0 -0
  139. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/domains/users/test_create_user_plugin.py +0 -0
  140. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/domains/users/test_delete_user_plugin.py +0 -0
  141. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/domains/users/test_get_me_plugin.py +0 -0
  142. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/domains/users/test_get_user_by_id_plugin.py +0 -0
  143. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/domains/users/test_get_users_plugin.py +0 -0
  144. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/domains/users/test_login_plugin.py +0 -0
  145. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/domains/users/test_logout_plugin.py +0 -0
  146. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/domains/users/test_update_user_plugin.py +0 -0
  147. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/domains/users/test_welcome_service_plugin.py +0 -0
  148. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/helpers/async_wait.py +0 -0
  149. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/helpers/mock_db.py +0 -0
  150. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/helpers/trace_chains.py +0 -0
  151. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/linters/test_discovery_naming_linter.py +0 -0
  152. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/linters/test_doc_path_linter.py +0 -0
  153. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/linters/test_domain_isolation_linter.py +0 -0
  154. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/linters/test_event_contract_linter.py +0 -0
  155. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/linters/test_field_divergence_linter.py +0 -0
  156. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/linters/test_route_collision_linter.py +0 -0
  157. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/linters/test_table_ownership_linter.py +0 -0
  158. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/linters/test_tool_doc_drift_linter.py +0 -0
  159. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_catalog.py +0 -0
  160. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_chaos_control.py +0 -0
  161. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_chaos_tool.py +0 -0
  162. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_cli.py +0 -0
  163. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_durable_one_shots.py +0 -0
  164. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_event_schemas_plugin.py +0 -0
  165. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_scaffold.py +0 -0
  166. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_system_events_stats.py +0 -0
  167. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_system_traces_plugin.py +0 -0
  168. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_trace_chain_helper.py +0 -0
  169. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/system/test_upgrade.py +0 -0
  170. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/config/test_config_tool.py +0 -0
  171. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/context/test_context_tool.py +0 -0
  172. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/event_bus/test_event_bus_broker_parity.py +0 -0
  173. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/event_bus/test_event_bus_groups.py +0 -0
  174. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/event_bus/test_event_bus_kafka_parity.py +0 -0
  175. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/event_bus/test_event_bus_rabbitmq_parity.py +0 -0
  176. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/event_bus/test_event_bus_tool.py +0 -0
  177. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/event_bus/test_redis_streams_driver.py +0 -0
  178. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/http_server/test_http_params_hardening.py +0 -0
  179. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/http_server/test_http_server_tool.py +0 -0
  180. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/http_server/test_security_hardening.py +0 -0
  181. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/logger/test_logger_tool.py +0 -0
  182. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/registry/test_registry_tool.py +0 -0
  183. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/s3/test_s3_parity.py +0 -0
  184. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/sqlite/test_sqlite_concurrency.py +0 -0
  185. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/sqlite/test_sqlite_describe_schema.py +0 -0
  186. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/sqlite/test_sqlite_driver.py +0 -0
  187. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/sqlite/test_sqlite_migrations.py +0 -0
  188. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/sqlite/test_sqlite_tool.py +0 -0
  189. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/state/test_state_parity.py +0 -0
  190. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/state/test_state_tool.py +0 -0
  191. {microcoreos-0.3.2 → microcoreos-0.4.0}/tests/tools/telemetry/test_telemetry_tool.py +0 -0
  192. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/config/config_tool.py +0 -0
  193. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/context/authoring_guide.md +0 -0
  194. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/context/context_tool.py +0 -0
  195. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/context/renderers.py +0 -0
  196. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/context/scanners.py +0 -0
  197. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/event_bus/drivers.py +0 -0
  198. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/event_bus/envelope.py +0 -0
  199. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/event_bus/event_bus_tool.py +0 -0
  200. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/event_bus/redis_streams_driver.py +0 -0
  201. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/event_bus/sqlite_driver.py +0 -0
  202. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/http_server/types.py +0 -0
  203. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/logger/logger_tool.py +0 -0
  204. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/sqlite/errors.py +0 -0
  205. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/sqlite/sqlite_tool.py +0 -0
  206. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/sqlite/transaction.py +0 -0
  207. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/state/state_tool.py +0 -0
  208. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/system/registry_tool.py +0 -0
  209. {microcoreos-0.3.2 → microcoreos-0.4.0}/tools/telemetry/__init__.py +0 -0
  210. {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.2
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. Change `name = "db"` in the SQLite tool to `name = "sqlite"`
208
- 2. Change `name = "postgresql"` in the PostgreSQL tool to `name = "db"`
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. Change `name = "db"` in the SQLite tool to `name = "sqlite"`
167
- 2. Change `name = "postgresql"` in the PostgreSQL tool to `name = "db"`
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
+ );
@@ -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
- import asyncio
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 "postgresql"
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
- continue
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", "microcoreos")
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
- pytest.skip(
65
- "PostgreSQL not available — "
66
- "docker compose -f dev_infra/docker-compose.yml up -d postgres"
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", "microcoreos")
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
- await t.setup()
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()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "microcoreos"
3
- version = "0.3.2"
3
+ version = "0.4.0"
4
4
  description = "Atomic Microkernel Architecture optimized for AI-Driven Development"
5
5
  license = {text = "MIT"}
6
6
  readme = "README.md"
@@ -58,7 +58,7 @@ _TEST_ENV = {
58
58
  "PG_PORT": "5432",
59
59
  "PG_USER": "postgres",
60
60
  "PG_PASSWORD": "postgres",
61
- "PG_DATABASE": "microcoreos",
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
- pytest.skip(f"active db tool {tool_cls.__name__} is unreachable: {e}")
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", "microcoreos")
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
- pytest.skip(
57
- "PostgreSQL not available — "
58
- "docker compose -f dev_infra/docker-compose.yml up -d postgres"
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", "microcoreos")
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
- await sqlite_tool.shutdown()
239
- pytest.skip(
240
- "PostgreSQL not available — "
241
- "docker compose -f dev_infra/docker-compose.yml up -d postgres"
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