microcoreos 0.1.0__py3-none-any.whl

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 (112) hide show
  1. microcoreos/__init__.py +23 -0
  2. microcoreos/_template/.agent/skills/microcoreos-architecture/SKILL.md +24 -0
  3. microcoreos/_template/.agent/skills/microcoreos-architecture/agent.md +18 -0
  4. microcoreos/_template/.agent/workflows/feature-plan.md +111 -0
  5. microcoreos/_template/.agent/workflows/multi-domain-plan.md +96 -0
  6. microcoreos/_template/.agent/workflows/new-domain.md +201 -0
  7. microcoreos/_template/.agent/workflows/new-tool.md +72 -0
  8. microcoreos/_template/.dockerignore +23 -0
  9. microcoreos/_template/.env.example +96 -0
  10. microcoreos/_template/.gitignore +57 -0
  11. microcoreos/_template/AGENTS.md +175 -0
  12. microcoreos/_template/Dockerfile +35 -0
  13. microcoreos/_template/INSTRUCTIONS_FOR_AI.md +560 -0
  14. microcoreos/_template/dev_infra/cache_probe.py +176 -0
  15. microcoreos/_template/dev_infra/docker-compose.yml +137 -0
  16. microcoreos/_template/docs/CLI.md +187 -0
  17. microcoreos/_template/docs/CORE_INFRASTRUCTURE.md +372 -0
  18. microcoreos/_template/docs/ELASTIC_DEPLOYMENT.md +277 -0
  19. microcoreos/_template/docs/EVENT_BUS.md +288 -0
  20. microcoreos/_template/docs/HTTP_SERVER.md +98 -0
  21. microcoreos/_template/docs/INDEX.md +41 -0
  22. microcoreos/_template/docs/OBSERVABILITY.md +56 -0
  23. microcoreos/_template/docs/OBSERVABILITY_API.md +183 -0
  24. microcoreos/_template/docs/PARALLEL_DEVELOPMENT.md +494 -0
  25. microcoreos/_template/docs/PLAN_EVENT_LINTER.md +95 -0
  26. microcoreos/_template/docs/TECH_DEBT.md +388 -0
  27. microcoreos/_template/docs/translations/es/README.md +265 -0
  28. microcoreos/_template/domains/devtools/lint/plugin_sources.py +51 -0
  29. microcoreos/_template/domains/devtools/plugins/discovery_naming_linter_plugin.py +89 -0
  30. microcoreos/_template/domains/devtools/plugins/domain_isolation_linter_plugin.py +80 -0
  31. microcoreos/_template/domains/devtools/plugins/event_contract_linter_plugin.py +420 -0
  32. microcoreos/_template/domains/devtools/plugins/event_schemas_plugin.py +94 -0
  33. microcoreos/_template/domains/devtools/plugins/field_divergence_linter_plugin.py +178 -0
  34. microcoreos/_template/domains/devtools/plugins/plan_validator_plugin.py +581 -0
  35. microcoreos/_template/domains/devtools/plugins/route_collision_linter_plugin.py +51 -0
  36. microcoreos/_template/domains/devtools/plugins/table_ownership_linter_plugin.py +66 -0
  37. microcoreos/_template/domains/devtools/plugins/tool_doc_drift_linter_plugin.py +74 -0
  38. microcoreos/_template/domains/system/plugins/event_delivery_monitor_plugin.py +53 -0
  39. microcoreos/_template/domains/system/plugins/system_events_plugin.py +132 -0
  40. microcoreos/_template/domains/system/plugins/system_events_stream_plugin.py +46 -0
  41. microcoreos/_template/domains/system/plugins/system_logs_stream_plugin.py +47 -0
  42. microcoreos/_template/domains/system/plugins/system_metrics_plugin.py +85 -0
  43. microcoreos/_template/domains/system/plugins/system_status_plugin.py +66 -0
  44. microcoreos/_template/domains/system/plugins/system_traces_plugin.py +128 -0
  45. microcoreos/_template/domains/system/plugins/system_traces_stream_plugin.py +121 -0
  46. microcoreos/_template/domains/system/plugins/tool_health_plugin.py +53 -0
  47. microcoreos/_template/extras/available_domains/chaos/plugins/blocking_boot_plugin.py +19 -0
  48. microcoreos/_template/extras/available_domains/chaos/plugins/chaos_control_plugin.py +625 -0
  49. microcoreos/_template/extras/available_domains/chaos/plugins/failing_plugin.py +27 -0
  50. microcoreos/_template/extras/available_domains/chaos/plugins/stress_plugin.py +40 -0
  51. microcoreos/_template/extras/available_domains/ping/plugins/ping_plugin.py +36 -0
  52. microcoreos/_template/extras/available_domains/scheduler/migrations/001_scheduler_one_shots.sql +8 -0
  53. microcoreos/_template/extras/available_domains/scheduler/models/scheduler_one_shot.py +15 -0
  54. microcoreos/_template/extras/available_domains/scheduler/plugins/durable_one_shots_plugin.py +125 -0
  55. microcoreos/_template/extras/available_domains/users/migrations/001_create_users.sql +12 -0
  56. microcoreos/_template/extras/available_domains/users/models/user.py +31 -0
  57. microcoreos/_template/extras/available_domains/users/plugins/create_user_plugin.py +89 -0
  58. microcoreos/_template/extras/available_domains/users/plugins/get_me_plugin.py +63 -0
  59. microcoreos/_template/extras/available_domains/users/plugins/login_plugin.py +107 -0
  60. microcoreos/_template/extras/available_domains/users/plugins/logout_plugin.py +26 -0
  61. microcoreos/_template/extras/available_tools/auth/auth_tool.py +132 -0
  62. microcoreos/_template/extras/available_tools/chaos/chaos_tool.py +32 -0
  63. microcoreos/_template/extras/available_tools/kafka/kafka_driver.py +452 -0
  64. microcoreos/_template/extras/available_tools/postgresql/postgresql_tool.py +882 -0
  65. microcoreos/_template/extras/available_tools/rabbitmq/rabbitmq_driver.py +303 -0
  66. microcoreos/_template/extras/available_tools/redis_state/redis_state_tool.py +253 -0
  67. microcoreos/_template/extras/available_tools/s3/__init__.py +0 -0
  68. microcoreos/_template/extras/available_tools/s3/s3_tool.py +426 -0
  69. microcoreos/_template/extras/available_tools/scheduler/scheduler_tool.py +328 -0
  70. microcoreos/_template/main.py +9 -0
  71. microcoreos/_template/plans/PILOT.md +99 -0
  72. microcoreos/_template/plans/README.md +236 -0
  73. microcoreos/_template/plans/active_plan.md +25 -0
  74. microcoreos/_template/plans/active_plan.yaml +43 -0
  75. microcoreos/_template/tools/config/config_tool.py +110 -0
  76. microcoreos/_template/tools/context/authoring_guide.md +305 -0
  77. microcoreos/_template/tools/context/context_tool.py +161 -0
  78. microcoreos/_template/tools/context/renderers.py +118 -0
  79. microcoreos/_template/tools/context/scanners.py +267 -0
  80. microcoreos/_template/tools/event_bus/drivers.py +108 -0
  81. microcoreos/_template/tools/event_bus/envelope.py +55 -0
  82. microcoreos/_template/tools/event_bus/event_bus_tool.py +506 -0
  83. microcoreos/_template/tools/event_bus/redis_streams_driver.py +300 -0
  84. microcoreos/_template/tools/event_bus/sqlite_driver.py +316 -0
  85. microcoreos/_template/tools/http_server/context.py +120 -0
  86. microcoreos/_template/tools/http_server/http_server_tool.py +558 -0
  87. microcoreos/_template/tools/http_server/pipeline.py +267 -0
  88. microcoreos/_template/tools/logger/logger_tool.py +96 -0
  89. microcoreos/_template/tools/sqlite/errors.py +152 -0
  90. microcoreos/_template/tools/sqlite/migrations.py +140 -0
  91. microcoreos/_template/tools/sqlite/sqlite_tool.py +591 -0
  92. microcoreos/_template/tools/sqlite/transaction.py +189 -0
  93. microcoreos/_template/tools/state/state_tool.py +173 -0
  94. microcoreos/_template/tools/system/registry_tool.py +90 -0
  95. microcoreos/_template/tools/telemetry/__init__.py +0 -0
  96. microcoreos/_template/tools/telemetry/telemetry_tool.py +143 -0
  97. microcoreos/base_plugin.py +20 -0
  98. microcoreos/base_tool.py +48 -0
  99. microcoreos/catalog.py +268 -0
  100. microcoreos/cli.py +206 -0
  101. microcoreos/container.py +222 -0
  102. microcoreos/context.py +6 -0
  103. microcoreos/kernel.py +223 -0
  104. microcoreos/project_readme.md +107 -0
  105. microcoreos/registry.py +53 -0
  106. microcoreos/scaffold.py +218 -0
  107. microcoreos/upgrade.py +547 -0
  108. microcoreos-0.1.0.dist-info/METADATA +366 -0
  109. microcoreos-0.1.0.dist-info/RECORD +112 -0
  110. microcoreos-0.1.0.dist-info/WHEEL +4 -0
  111. microcoreos-0.1.0.dist-info/entry_points.txt +2 -0
  112. microcoreos-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,560 @@
1
+ # 🤖 AI Agent Implementation Guide — Advanced Reference
2
+
3
+ > **For building plugins, read `AI_CONTEXT.md` only.** This file is for advanced topics: testing, observability, creating tools, and edge cases.
4
+
5
+ ## ❌ Anti-Patterns (Common AI Mistakes)
6
+
7
+ These are the most frequent errors. Check these first before writing any code.
8
+
9
+ | Wrong | Correct | Why |
10
+ |-------|---------|-----|
11
+ | `async def on_event(self, data):` | `async def on_event(self, event: EventEnvelope):` | Subscribers now receive the full envelope, not raw data |
12
+ | `from domains.users.models.user import UserEntity` inside `orders` plugin | Define `OrderData` inline | No cross-domain imports |
13
+ | `class CreateUserRequest(BaseModel): name: str` (bare field) | `name: str = Field(min_length=1)` | FastAPI won't validate without constraints |
14
+ | `add_endpoint("/users", "POST", self.execute, ...)` without `response_model=` | Always pass `response_model=CreateUserResponse` | No OpenAPI docs generated |
15
+ | Logic inside `__init__` | Move to `on_boot()` or `execute()` | `__init__` is DI only |
16
+ | `import asyncio; asyncio.run(...)` inside a plugin | `await` or schedule via `scheduler` | Already inside an event loop |
17
+ | `?` placeholders in SQL | `$1, $2, $3...` | PostgreSQL-style; SQLite converts automatically |
18
+ | Returning the full Entity object (including `password_hash`) | Define a response schema with only the fields you expose | Leaks sensitive data |
19
+ | `from tools.http_server.http_server_tool import HttpServerTool` | Use DI: `def __init__(self, http)` | Hardcoded imports break tool swapping |
20
+ | Expecting automatic Kernel retries | Handle your own exceptions or use Tool-level resilience | **ToolProxy NO LONGER retries automatically** to prevent duplicates |
21
+ | `return {"success": False, "error": str(e)}` | `logger.error(e); return {"success": False, "error": "Generic Message"}` | `str(e)` leaks table names, paths, and internal logic |
22
+ | `if "UNIQUE" in str(e):` (deciding by the error's text) | `if getattr(e, "kind", None) == "unique_violation":` | Each engine words its errors differently; the db tool classifies them into a closed vocabulary so the branch survives a tool swap |
23
+ | `bus.publish("x.y", {"id": 1})` (raw dict) | Define `XyPayload(BaseModel)` and publish `XyPayload(id=1).model_dump()` | The publisher owns the event contract; raw dicts are flagged `UNTYPED_PAYLOAD` by `/system/lint` |
24
+ | `await asyncio.sleep(0.1)` in a test, then asserting async delivery | `await wait_until(lambda: <condition>)` from `tests/helpers/async_wait.py` | A fixed sleep guesses a duration and flakes under CI CPU contention; polling passes as soon as delivery lands |
25
+
26
+ ---
27
+
28
+ ## ⚠️ CRITICAL RULES (DO NOT IGNORE)
29
+
30
+ 1. **`main.py` is sacred** — Never modify. It only boots the Kernel.
31
+ 2. **Event Contract** — Subscribers receive `EventEnvelope`. Access data via `event.payload`.
32
+ 3. **No Hidden Magic** — The Kernel does NOT retry failed tool calls. Idempotency is your responsibility.
33
+ 4. **Safe Errors** — NEVER return `str(e)` to the client. Return safe, generic messages only.
34
+ 5. **Event Bus Power** — Leverage `ttl`, `retries`, and `backoff` in `subscribe()` and `publish()`.
35
+ 6. **DLQ Monitoring** — Final failures go to `_dlq.<event>`. Subscribe to it for error handling.
36
+ 7. **No framework patterns** — No Routers, Controllers, or Services. Only Tools (infrastructure) and Plugins (business logic).
37
+ 8. **No cross-domain imports** — Domains communicate exclusively via `event_bus`.
38
+ 9. **CSRF Guard** — HTTP mutations (POST/PUT/DELETE) via cookie auth REQUIRE `X-Requested-With` header.
39
+ 10. **Secure Cookies** — `context.set_cookie` defaults to `Secure=True`, `HttpOnly=True`, `SameSite=Lax`.
40
+ 11. **Return format**: Always `{"success": bool, "data": ..., "error": ...}`.
41
+
42
+ ---
43
+
44
+ ## 🧭 Navigation
45
+
46
+ | Task | Section |
47
+ |---|---|
48
+ | New feature on existing domain | [Plugin](#-new-plugin) |
49
+ | New functional area from scratch | [Domain](#-new-domain) |
50
+ | New infrastructure capability | [Tool](#-new-tool) |
51
+
52
+ ---
53
+
54
+ ## 🧩 New Domain
55
+
56
+ Folder structure:
57
+ ```
58
+ domains/{name}/
59
+ __init__.py
60
+ models/{name}.py ← DB Entity only (mirrors the table)
61
+ migrations/001_xxx.sql ← Raw SQL, auto-executed on boot
62
+ plugins/ ← 1 file = 1 feature
63
+ ```
64
+
65
+ ---
66
+
67
+ ## ⚡ New Plugin
68
+
69
+ **Location**: `domains/{domain}/plugins/{feature}_plugin.py`
70
+ **Rule**: 1 File = 1 Feature. Schema defined inline.
71
+
72
+ ```python
73
+ from typing import Optional
74
+ from pydantic import BaseModel, Field
75
+ from microcoreos import BasePlugin
76
+
77
+ class CreateProductPlugin(BasePlugin):
78
+ def __init__(self, logger, event_bus, http, db):
79
+ self.logger = logger
80
+ self.bus = event_bus
81
+ self.http = http
82
+ self.db = db
83
+
84
+ async def on_boot(self):
85
+ self.http.add_endpoint(
86
+ path="/products", method="POST",
87
+ handler=self.execute,
88
+ tags=["Products"]
89
+ )
90
+ # Leverage built-in retries for event subscribers
91
+ await self.bus.subscribe("order.created", self.on_order_created, retries=3, backoff=1.0)
92
+
93
+ async def execute(self, data: dict, context=None):
94
+ try:
95
+ # Action Phase
96
+ # publish is fire-and-forget. Use ttl for expiring messages.
97
+ await self.bus.publish("product.created", {"id": 123}, ttl=3600)
98
+ return {"success": True, "data": {"id": 123}}
99
+ except Exception as e:
100
+ # Kernel won't retry db.execute! Handle idempotency here.
101
+ self.logger.error(f"Failed to create product: {e}")
102
+ return {"success": False, "error": "Could not create product"}
103
+
104
+ async def on_order_created(self, event) -> None:
105
+ # event is an EventEnvelope
106
+ self.logger.info(f"Order received: {event.payload}")
107
+ # To participate in request() RPC, return a dict
108
+ return {"processed": True}
109
+ ```
110
+
111
+ ---
112
+
113
+ ## 📨 Event Payload Schemas (Schema Registry Readiness)
114
+
115
+ Every event a plugin publishes has a Pydantic payload model, defined **inline in
116
+ the publisher plugin** — same place as its request/response schemas. The
117
+ publisher owns the event contract, exactly like a producer registers its schema
118
+ in a schema registry.
119
+
120
+ ```python
121
+ # ── In the PUBLISHER plugin ─────────────────────────
122
+ class OrderCreatedPayload(BaseModel):
123
+ id: int
124
+ user_id: int
125
+ total: float
126
+
127
+ await self.bus.publish("order.created", OrderCreatedPayload(
128
+ id=order_id, user_id=req.user_id, total=req.total
129
+ ).model_dump())
130
+ ```
131
+
132
+ ```python
133
+ # ── In a CONSUMER plugin (another domain) ───────────
134
+ # Never import the publisher's model (no cross-domain imports).
135
+ # Declare ONLY the fields this consumer needs — tolerant reader.
136
+ class OrderForBilling(BaseModel):
137
+ id: int
138
+ total: float
139
+
140
+ async def on_order_created(self, event):
141
+ order = OrderForBilling(**event.payload) # extra keys are ignored
142
+ ```
143
+
144
+ Rules of the pattern:
145
+
146
+ - **Publish with a bare `.model_dump()`** — no arguments. `exclude_none=True`
147
+ and friends can drop keys at runtime, so the linter refuses to trust them
148
+ (`UNKNOWN_PAYLOAD`).
149
+ - **Consumers are tolerant readers**: they re-declare only the fields they
150
+ read. This is not duplication — it is what decouples publisher evolution
151
+ from consumers, and the `EventContractLinterPlugin` statically cross-checks
152
+ that every field a consumer requires exists in every publish site.
153
+ - **Raw-dict publishes still work** but are flagged `UNTYPED_PAYLOAD`
154
+ (info, advisory) in `GET /system/lint`.
155
+ - **Why**: when the event bus is swapped to Kafka (Roadmap Issue 18), each
156
+ payload model's `model_json_schema()` is the JSON Schema the registry needs —
157
+ the contracts are already written, no plugin changes. `GET /system/events/schemas`
158
+ serves the full catalog (event → JSON Schema) today.
159
+
160
+ ---
161
+
162
+ ## 🔧 New Tool
163
+
164
+ **Location**: `tools/{name}/{name}_tool.py`
165
+ **Rule**: Stateless, isolated, self-documented. Use `EventBusDriver` pattern for new transport layers.
166
+
167
+ ### The Parity Rule (Contract over Implementation)
168
+ Any replacement tool (e.g., swapping SQLite for PostgreSQL, or In-Memory State
169
+ for Redis) MUST pass the **Parity Suite** of the reference implementation it
170
+ replaces. This ensures that plugins remain infrastructure-blind and behavior
171
+ is consistent across backends.
172
+
173
+ **Canonical examples:**
174
+ - `tests/tools/test_state_parity.py`: Verifies that `RedisStateTool` behaves
175
+ exactly like the default in-memory `StateTool`.
176
+ - `tests/tools/test_event_bus_broker_parity.py`: Parametrized suite that
177
+ runs against both the local driver and `RedisStreamsDriver`.
178
+
179
+ **Health contract (optional, only for tools with an external backend)**:
180
+ If the tool talks to an external service (DB, S3, broker...), make its
181
+ connection-error class inherit `ToolUnavailableError` so ToolProxy marks it
182
+ DEAD on the first infrastructure failure:
183
+
184
+ ```python
185
+ from microcoreos import BaseTool, ToolUnavailableError
186
+
187
+ class RedisError(Exception): ... # business errors
188
+ class RedisConnectionError(RedisError, ToolUnavailableError): ... # infra -> DEAD immediately
189
+ ```
190
+
191
+ In-memory/local tools (state, logger, scheduler...) skip this entirely — the
192
+ fallback (DEAD after 5 consecutive failures) covers every tool automatically.
193
+
194
+ ---
195
+
196
+ ## 🚦 Rate Limiting Pattern
197
+
198
+ There is **no `rate_limiter` tool, by design**. Rate limiting splits into two
199
+ layers, and neither needs one:
200
+
201
+ - **Volumetric / anonymous (per-IP, anti-abuse, DDoS)** → belongs to the edge
202
+ (nginx, gateway, CDN), never to the monolith. See the edge section in
203
+ `docs/ELASTIC_DEPLOYMENT.md`.
204
+ - **Identity-aware (per-user, per-API-key, per-plan quotas)** → business
205
+ policy, implemented in the plugin with the `state` tool primitive:
206
+
207
+ ```python
208
+ async def execute(self, data: dict, context=None):
209
+ attempts = await self.state.increment(
210
+ f"login:{req.email}", namespace="rate_limit", ttl=900 # 15-min window
211
+ )
212
+ if attempts > 5:
213
+ context.set_status(429)
214
+ context.set_header("Retry-After", "900")
215
+ return {"success": False, "error": "Too many attempts. Try again later."}
216
+ # ... normal handling
217
+ ```
218
+
219
+ Rules of the pattern:
220
+
221
+ - **Key by identity** (user id, email, API key) — never by IP (that is the
222
+ edge's job).
223
+ - **Fixed window**: `increment()` applies the TTL only when the key is
224
+ created, so the counter resets `ttl` seconds after the first hit. A client
225
+ can burst up to 2× the limit across a window boundary — acceptable for
226
+ quotas and throttles. If a real case ever demands sliding-window precision,
227
+ that is the moment to introduce a tool, not before.
228
+ - **`Retry-After`**: the state contract does not expose remaining TTL, so use
229
+ the window size as a conservative upper bound.
230
+ - **Distribution is free**: with `RedisStateTool` swapped in
231
+ (`extras/available_tools/redis_state/`), the same code enforces the limit
232
+ across all replicas.
233
+
234
+ ---
235
+
236
+ ## 🔐 User Roles & Authorization Pattern
237
+
238
+ Roles are **business data**, not infrastructure. The `auth` tool remains
239
+ infrastructure-blind (it only handles signing/verifying strings), while the
240
+ `users` domain manages the roles.
241
+
242
+ - **Storage**: roles are a column in the `users` table (JSON array in SQLite).
243
+ - **Claims**: `LoginPlugin` fetches roles and includes them in the JWT claims.
244
+ - **Consumption**: plugins read `data["_auth"]["roles"]` for general authorization.
245
+
246
+ ### The Hybrid Authorization Rule
247
+ For standard operations, trust the claims in the token. For **critical
248
+ operations** (financial transactions, privilege escalation, destructive
249
+ actions), perform a **fresh check** against the database to catch late
250
+ revocations or role changes that happened after the token was issued:
251
+
252
+ ```python
253
+ async def execute(self, data: dict, context=None):
254
+ roles = data.get("_auth", {}).get("roles", [])
255
+
256
+ # 1. Fast check (claims)
257
+ if "admin" not in roles:
258
+ return {"success": False, "error": "Forbidden"}
259
+
260
+ # 2. Fresh check (critical ops only)
261
+ user_id = data["_auth"]["sub"]
262
+ user = await self.db.query_one("SELECT roles FROM users WHERE id = $1", [user_id])
263
+ fresh_roles = json.loads(user["roles"])
264
+ if "admin" not in fresh_roles:
265
+ return {"success": False, "error": "Privileges revoked"}
266
+
267
+ # ... proceed
268
+ ```
269
+
270
+ ---
271
+
272
+ ## 🧪 Testing
273
+
274
+ Constructor injection makes testing straightforward. In this project, we support and encourage two levels of testing:
275
+
276
+ ### Which level? One rule
277
+
278
+ **Mock the input and the output. Use the real tool the moment you assert persistence.**
279
+
280
+ | What the test asserts | Level | Why |
281
+ |---|---|---|
282
+ | Returned dict, branch logic, error paths | **Mock** (level 2) | Nothing durable is being claimed; a real DB adds setup and no confidence |
283
+ | Rows in a table, state after a swap, an event a consumer actually received | **Real tool** (level 1) | The claim IS the persisted state — see below |
284
+
285
+ The reason the second row is not negotiable: a mocked `db` lets you assert
286
+ *"`execute` was called with this SQL string"*. That verifies the plugin talks
287
+ to a contract; it does **not** verify the SQL works against the schema. A
288
+ typo'd column, a `NOT NULL` the migration declares and the INSERT never fills,
289
+ a type SQLite accepts and PostgreSQL rejects — every one of those passes a
290
+ mocked test and fails in production. Once you assert on a table, you need the
291
+ table.
292
+
293
+ A plugin that both validates and writes is not a dilemma: test the validation
294
+ branches with mocks and the write with the real tool. They are different tests.
295
+
296
+ **Most plugin tests here are mocked, and that is correct** — the majority of
297
+ plugins are input → output. Reach for level 1 when the subject is the durable
298
+ effect, not by default.
299
+
300
+ **What mocks cannot catch, and what covers it.** A mocked test verifies the
301
+ plugin against a contract the test itself declares, so if a tool's real API
302
+ drifts the mock keeps passing. That gap is closed elsewhere, not by your test:
303
+ the `ToolDocDriftLinter` (boot + `/system/lint`) compares each tool's
304
+ documented interface against its implementation, and the parity suites
305
+ (`tests/tools/test_state_parity.py`,
306
+ `tests/tools/test_event_bus_broker_parity.py`) hold swappable tools to one
307
+ behaviour. Do not re-verify that in a plugin test.
308
+
309
+ ### Ask for tools the way the plugin does
310
+
311
+ A plugin never builds a tool: it names one in `__init__` and the Kernel hands
312
+ it over. pytest injects by parameter name too, so **a test uses the same
313
+ vocabulary** — `tests/conftest.py` publishes one fixture per Kernel injection
314
+ key (`db`, `event_bus`, `auth`, `state`, `logger`, `config`). No tool imports,
315
+ no setup/teardown, no hand-built schema:
316
+
317
+ ```python
318
+ @pytest.mark.migrations("users") # the domain's REAL migrations
319
+ async def test_create_user_persists(db, event_bus, auth, logger):
320
+ plugin = CreateUserPlugin(http=MagicMock(), db=db,
321
+ event_bus=event_bus, auth=auth, logger=logger)
322
+ result = await plugin.execute({"name": "Ana", "email": "a@b.com", "password": "secret123"})
323
+
324
+ assert result["success"] is True
325
+ rows = await db.query("SELECT name, email, password_hash FROM users")
326
+ assert rows[0]["password_hash"] != "secret123"
327
+ ```
328
+
329
+ Worked example: `tests/test_plugin_di_fixtures.py`.
330
+
331
+ - `@pytest.mark.migrations("users", "system")` applies those domains' real
332
+ migration files. **No marker = a real but empty database**, which is what a
333
+ test that never touches a table wants.
334
+ - The `db` fixture resolves the **active** db tool, so these tests keep working
335
+ after a PostgreSQL swap instead of dying at collection on a hardcoded import.
336
+ - Fixtures drive setup/shutdown tolerating sync OR async, exactly as the Kernel
337
+ does — `LoggerTool.shutdown()` is sync, `SqliteTool.shutdown()` is not.
338
+ - Need something these do not give you? Declare your own fixture with that
339
+ name; a local one always overrides the shared one.
340
+
341
+ ### Deliberate constraint divergence
342
+
343
+ The `FieldDivergenceLinter` warns when sibling plugins in a domain validate the
344
+ same field differently. When that is on purpose, record it where it happens —
345
+ do not leave the warning standing:
346
+
347
+ ```python
348
+ password: str = Field(
349
+ min_length=1,
350
+ json_schema_extra={"divergence_ok": "login verifies against the hash"},
351
+ )
352
+ ```
353
+
354
+ The reason is required (an empty one is ignored), and the waived declaration
355
+ drops out of the comparison — the others are still compared with each other.
356
+
357
+ **Prefabs — read these before hand-rolling setup:**
358
+
359
+ | Helper | For |
360
+ |---|---|
361
+ | `tests/helpers/mock_db.py` | `async with db.transaction()` — a bare `AsyncMock` crashes there. `TxMock` / `FailingTxMock` |
362
+ | `tests/helpers/active_db.py` | A real DB tool with the domain's real migrations applied |
363
+ | `tests/helpers/async_wait.py` | `wait_until(...)` instead of a fixed sleep. Pass `describe=` so a rare timeout reports the observed state, not `<lambda at 0x...>` |
364
+ | `tests/helpers/trace_chains.py` | Asserting a full event chain across domains |
365
+
366
+ ### 1. Black-Box Integration Testing (Recommended for DB/State/Events)
367
+ To prevent fragile mocks and ensure database queries actually run and pass syntax and dialect checks:
368
+ - Do NOT mock `db`, `event_bus`, or `state` tools unless absolutely necessary.
369
+ - Use the real `SqliteTool` configured with `:memory:` (fast, self-contained, no external database files needed).
370
+ - Apply migrations dynamically to the in-memory database to establish the real schema.
371
+ - Assert on the final state of the database and event bus (Black-Box) rather than internal mock call assertions.
372
+
373
+ Example of a Black-Box Integration Test:
374
+ ```python
375
+ import pytest
376
+ from tools.sqlite.sqlite_tool import SqliteTool
377
+ from tools.event_bus.event_bus_tool import EventBusTool
378
+ from tools.auth.auth_tool import AuthTool
379
+ from domains.users.plugins.create_user_plugin import CreateUserPlugin
380
+
381
+ @pytest.fixture
382
+ async def test_env():
383
+ # Setup real db in memory
384
+ db = SqliteTool()
385
+ db._db_path = ":memory:"
386
+ await db.setup()
387
+
388
+ # Run the migrations needed for this domain
389
+ with open("domains/<your_domain>/migrations/001_create_<table>.sql", "r") as f:
390
+ await db.execute(f.read())
391
+
392
+ bus = EventBusTool()
393
+ await bus.setup()
394
+
395
+ auth = AuthTool()
396
+ await auth.setup()
397
+
398
+ yield db, bus, auth
399
+
400
+ await db.shutdown()
401
+ await bus.shutdown()
402
+
403
+ @pytest.mark.anyio
404
+ async def test_create_user_integration(test_env):
405
+ db, bus, auth = test_env
406
+ plugin = CreateUserPlugin(http=None, db=db, event_bus=bus, logger=None, auth=auth)
407
+
408
+ # Input
409
+ data = {"name": "John Doe", "email": "john@example.com", "password": "password123"}
410
+
411
+ # Execute (Black Box)
412
+ result = await plugin.execute(data)
413
+
414
+ # Output Verification
415
+ assert result["success"] is True
416
+ assert result["data"]["email"] == "john@example.com"
417
+
418
+ # Database Verification (No Mocks!)
419
+ users = await db.query("SELECT * FROM users WHERE email = $1", ["john@example.com"])
420
+ assert len(users) == 1
421
+ assert users[0]["name"] == "John Doe"
422
+ ```
423
+
424
+ ### Waiting for Async Delivery (Events, Retries, DLQ)
425
+
426
+ Never assert right after a fixed `await asyncio.sleep(...)`. A fixed sleep
427
+ guesses how long delivery takes; under CI CPU contention the guess loses and
428
+ the test flakes. Poll the real condition instead:
429
+
430
+ ```python
431
+ from tests.helpers.async_wait import wait_for_dlq, wait_until
432
+
433
+ await bus.publish("user.created", payload)
434
+ await wait_until(lambda: len(received) == 1) # returns as soon as delivery lands
435
+ ```
436
+
437
+ For **DLQ events** always use `wait_for_dlq(bus, "user.created")`, never plain
438
+ `wait_until` and never a hand-picked timeout: the DLQ only fires after retries
439
+ exhaust their exponential backoff, which can exceed `wait_until`'s default
440
+ timeout — the helper derives its deadline from the retries/backoff the
441
+ subscribers declared.
442
+
443
+ The one legitimate fixed sleep is a **negative check** (asserting that nothing
444
+ arrives) — there is no condition to poll for, so a short fixed wait is correct
445
+ there.
446
+
447
+ For asserting a full event chain across domains (flow tests), use the chain
448
+ helper in `tests/helpers/trace_chains.py`.
449
+
450
+ ### 2. Isolated Unit Testing (For pure business logic / third-party mocks)
451
+ If you only need to verify branch/flow logic or if you have complex external dependencies (e.g., third-party APIs):
452
+ - Mock the specific tools using `unittest.mock.AsyncMock` or `MagicMock`.
453
+ - Keep assertions focused on inputs/outputs and mock side-effects.
454
+ - Any method the plugin uses as `async with` must be overridden with
455
+ `MagicMock` — a bare `AsyncMock` yields a coroutine there and crashes. For
456
+ `db.transaction()` use the prefab helpers:
457
+ `db.transaction = MagicMock(return_value=TxMock())` (happy path) or
458
+ `FailingTxMock()` (sad path), from `tests.helpers.mock_db`.
459
+
460
+ Example:
461
+ ```python
462
+ from unittest.mock import AsyncMock, MagicMock
463
+ from domains.users.plugins.create_user_plugin import CreateUserPlugin
464
+
465
+ @pytest.mark.anyio
466
+ async def test_create_user_unit():
467
+ mock_db = AsyncMock(execute=AsyncMock(return_value=42))
468
+ plugin = CreateUserPlugin(
469
+ http=MagicMock(), db=mock_db, event_bus=AsyncMock(),
470
+ logger=MagicMock(), auth=MagicMock(hash_password=AsyncMock(return_value="hashed"))
471
+ )
472
+ result = await plugin.execute({"name": "Test", "email": "a@b.com", "password": "p"})
473
+ assert result["success"] is True
474
+ assert result["data"]["id"] == 42
475
+ ```
476
+ ---
477
+
478
+ ## 📦 Available Extras
479
+
480
+ The project includes pre-built tools and domains in the `extras/` folder. These
481
+ are not active by default to keep the core lean.
482
+
483
+ ### Activating an Extra
484
+
485
+ ```bash
486
+ microcoreos add postgres
487
+ ```
488
+
489
+ One command does all three acts: installs the optional dependency, moves the
490
+ source out of `extras/` into `tools/` (and `domains/` for a pair), and appends
491
+ the extra's settings to `.env` without overwriting values already there. Run
492
+ `microcoreos add` with no argument to list what is available.
493
+
494
+ By hand it is the same three acts, and each one fails in a different place:
495
+
496
+ ```bash
497
+ uv add 'microcoreos[postgres]' # 1. the library
498
+ mv extras/available_tools/postgresql tools/ # 2. the source you now own
499
+ # 3. the settings — see the table below
500
+ ```
501
+
502
+ Skipping step 1 does not fail quietly — the Kernel reports a load error for
503
+ that file at boot (`No module named 'asyncpg'`). Skipping step 2 does nothing
504
+ at all: the Kernel only discovers what is under `tools/` and `domains/`.
505
+
506
+ | Extra | Package step | Folder step |
507
+ |---|---|---|
508
+ | **PostgreSQL** — takes over the `db` key; set `DATABASE_URL` in `.env` | `uv add 'microcoreos[postgres]'` | `mv extras/available_tools/postgresql tools/` |
509
+ | **Redis State** — swaps in-memory `state` for a distributed one | `uv add 'microcoreos[redis]'` | `mv extras/available_tools/redis_state tools/` |
510
+ | **S3** — object storage, private bucket + presigned URLs | `uv add 'microcoreos[s3]'` | `mv extras/available_tools/s3 tools/` |
511
+ | **Scheduler** — cron + in-memory one-shots | `uv add 'microcoreos[scheduler]'` | `mv extras/available_tools/scheduler tools/` |
512
+ | **Chaos** — resilience testing (tool + its domain) | none | `mv extras/available_tools/chaos tools/` and `mv extras/available_domains/chaos domains/` |
513
+
514
+ **Event Bus drivers** install by dropping the `*_driver.py` into
515
+ `tools/event_bus/` and setting `EVENT_BUS_DRIVER`: `kafka`
516
+ (`uv add 'microcoreos[kafka]'`), `rabbitmq` (`[rabbitmq]`). Redis Streams and
517
+ the durable SQLite driver already ship in `tools/event_bus/` — Redis Streams
518
+ still needs `uv add 'microcoreos[redis]'`.
519
+
520
+ **Domains that need a tool.** A plugin cannot exist without its tool, so a
521
+ domain extra always requires its tool extra first. `extras/available_domains/scheduler/`
522
+ (durable one-shots, the `scheduler.one_shot.*` bus API) needs the scheduler
523
+ tool installed by both steps above; without it the boot says
524
+ `Plugin scheduler.DurableOneShotsPlugin aborted: Missing tools: scheduler`.
525
+
526
+ ---
527
+
528
+ ## 📡 Observability Capabilities
529
+
530
+ ### Tool call metrics via `registry`
531
+ Every tool call is timed by ToolProxy. Access last 1000 records via `registry.get_metrics()`.
532
+
533
+ ### Native Pydantic Tracing
534
+ `event_bus.get_trace_history()` returns `List[TraceRecord]` (last 500 events).
535
+ Each `TraceRecord` has:
536
+ - `envelope`: The full `EventEnvelope` (metadata + payload).
537
+ - `subscribers`: List of names of handlers that received the event.
538
+
539
+ ---
540
+
541
+ ## 🗄️ Swapping the Database Engine
542
+ The `db` injection key is the contract. Plugins use `$1, $2...` placeholders. SQLite converts them internally to `?`.
543
+
544
+ ## 🗄️ Migration Dependency Ordering
545
+ The kernel **always** applies migrations via topological sort. Without `-- depends:`, the order is the discovery order (alphabetical by domain → alphabetical by filename). When a migration requires another to have run first, declare it on the first comment line:
546
+
547
+ ```sql
548
+ -- depends: users/001_create_users.sql
549
+ CREATE TABLE IF NOT EXISTS orders (
550
+ id INTEGER PRIMARY KEY,
551
+ user_id INTEGER REFERENCES users(id),
552
+ ...
553
+ );
554
+ ```
555
+
556
+ This works for same-domain or cross-domain dependencies. The `.sql` extension is optional in the depends value.
557
+
558
+ ---
559
+
560
+ *`AI_CONTEXT.md` is auto-generated on every boot by the `context_manager` tool. It contains the live inventory of tools and domain models.*