diff --git a/.claude/architecture-handshake.md b/.claude/architecture-handshake.md index ce41bbdb07..c20af06794 100644 --- a/.claude/architecture-handshake.md +++ b/.claude/architecture-handshake.md @@ -1,8 +1,8 @@ @@ -25,6 +25,18 @@ installed_by: jonah - Handler implementations - Infrastructure adapters +## Platform-Wide Rules + +1. **No backwards compatibility** - Breaking changes always acceptable. No deprecation periods, shims, or migration paths. +2. **Delete old code immediately** - Never leave deprecated code "for reference." If unused, delete it. +3. **No speculative refactors** - Only make changes that are directly requested or clearly necessary. +4. **No silent schema changes** - All schema changes must be explicit and deliberate. +5. **Frozen event schemas** - All models crossing boundaries (events, intents, actions, envelopes, projections) must use `frozen=True`. Internal mutable state is fine. +6. **Explicit timestamps** - Never use `datetime.now()` defaults. Inject timestamps explicitly. +7. **No hardcoded configuration** - All config via `.env` or Pydantic Settings. No localhost defaults. +8. **Kafka is required infrastructure** - Use async/non-blocking patterns. Never block the calling thread waiting for Kafka acks. +9. **No `# type: ignore` without justification** - Requires explanation comment and ticket reference. + ## Rules the Agent Must Obey 1. **Nodes MUST be declarative** - `node.py` extends base class with NO custom logic @@ -33,11 +45,9 @@ installed_by: jonah 4. **Orchestrators emit, never return** - Cannot return `result` 5. **Contracts are source of truth** - YAML contracts define behavior 6. **Container injection required** - All services via `ModelONEXContainer` -7. **Never block on Kafka** - Kafka is optional; operations must succeed without it ## Non-Goals (DO NOT) -- ❌ No backwards compatibility - change freely, no deprecation periods - ❌ No versioned directories (`v1_0_0/`, `v2/`) - version through contract.yaml fields only - ❌ No convenience over correctness - contract violations fail loudly - ❌ No business logic in nodes - nodes coordinate, handlers compute @@ -48,11 +58,10 @@ installed_by: jonah - Custom logic in `node.py` beyond base class extension - Error handling in nodes (belongs in handlers) - Direct Kafka/Postgres calls in nodes (use effect handlers) -- Leaving deprecated code "for reference" - DELETE IT ## Layer Boundaries -```text +``` omnibase_core (contracts, models) ↑ imported by omnibase_spi (protocols) diff --git a/.github/workflows/check-handshake.yml b/.github/workflows/check-handshake.yml new file mode 100644 index 0000000000..f285ff4c34 --- /dev/null +++ b/.github/workflows/check-handshake.yml @@ -0,0 +1,53 @@ +name: Check Architecture Handshake + +on: + push: + branches: [main, develop] + paths: + - '.claude/architecture-handshake.md' + - '.github/workflows/check-handshake.yml' + pull_request: + branches: [main, develop] + paths: + - '.claude/architecture-handshake.md' + - '.github/workflows/check-handshake.yml' + workflow_dispatch: + +jobs: + check-handshake: + name: Check architecture handshake + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Checkout omnibase_core + uses: actions/checkout@v4 + with: + repository: OmniNode-ai/omnibase_core + # Pinned to omnibase_core main as of 2026-02-10. + # Update by running: gh api repos/OmniNode-ai/omnibase_core/commits/main --jq '.sha' + ref: 4dc7a0adf299e47eef57ab9950b0f0a587638a3e + path: omnibase_core + # OMNIBASE_CORE_TOKEN is required when omnibase_core is private. + # GITHUB_TOKEN only has read access to the current repo by default. + token: ${{ secrets.OMNIBASE_CORE_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Verify omnibase_core checkout + run: | + if [ ! -f omnibase_core/architecture-handshakes/check-handshake.sh ]; then + echo "::error::Failed to checkout omnibase_core or check-handshake.sh not found." + echo "" + echo "Diagnostic information:" + echo " omnibase_core directory exists: $([ -d omnibase_core ] && echo 'yes' || echo 'no')" + echo " omnibase_core is empty: $([ -z "$(ls -A omnibase_core 2>/dev/null)" ] && echo 'yes' || echo 'no')" + echo "" + echo "If omnibase_core is private, GITHUB_TOKEN does not have cross-repo access." + echo "Set the OMNIBASE_CORE_TOKEN repository secret with a PAT that has repo scope." + exit 1 + fi + + - name: Check architecture handshake + run: bash omnibase_core/architecture-handshakes/check-handshake.sh diff --git a/docs/decisions/adr-kafka-required-infrastructure.md b/docs/decisions/adr-kafka-required-infrastructure.md new file mode 100644 index 0000000000..168fb70786 --- /dev/null +++ b/docs/decisions/adr-kafka-required-infrastructure.md @@ -0,0 +1,38 @@ +# ADR: Kafka is Required Infrastructure (Rule #8) + +**Status**: Accepted +**Date**: 2026-02-10 +**Ticket**: OMN-2084 + +## Context + +The platform originally operated under the rule "Never block on Kafka — Kafka is optional; +operations must succeed without it." This was encoded in the architecture handshake as a +constraint that all repos observed. + +As the platform matured, Kafka became the sole event transport for inter-node communication, +projection publishing, DLQ routing, and wiring health monitoring. The "optional Kafka" +invariant was no longer accurate — every production deployment requires a running Kafka +cluster, and "graceful degradation" without Kafka meant silently dropping events. + +## Decision + +**Kafka is required infrastructure** (platform-wide rule #8). Specifically: + +- Connection failures during `start()` raise and must be treated as fatal by callers. +- There is no "degraded mode" — if Kafka is unreachable, the node cannot operate. +- Async/non-blocking patterns are still required to avoid blocking the calling thread + while waiting for Kafka acks. +- Resilience patterns (circuit breaker, retry with backoff) protect against *transient* + broker failures, not permanent unavailability. + +This replaces the previous rule "Never block on Kafka." + +## Consequences + +- Callers of `EventBusKafka.start()` and `ProviderKafkaProducer.create()` must not + swallow connection errors. +- Integration tests that previously mocked Kafka away need explicit opt-in for + Kafka-less testing via markers (`@pytest.mark.no_kafka`). +- The `localhost:9092` defaults in config models remain as development conveniences but + must be overridden in deployed environments (per rule #7: no hardcoded configuration). diff --git a/docs/operations/EVENT_BUS_OPERATIONS_RUNBOOK.md b/docs/operations/EVENT_BUS_OPERATIONS_RUNBOOK.md index 0b63aa7d0b..7e3ab8143a 100644 --- a/docs/operations/EVENT_BUS_OPERATIONS_RUNBOOK.md +++ b/docs/operations/EVENT_BUS_OPERATIONS_RUNBOOK.md @@ -12,7 +12,7 @@ The EventBusKafka provides production-grade message streaming using Apache Kafka - **Circuit breaker** for connection failure protection - **Retry with exponential backoff** on publish failures - **Dead letter queue (DLQ)** for failed message processing -- **Graceful degradation** when Kafka is unavailable +- **Resilience** against transient broker failures (per platform-wide rule #8: Kafka is required infrastructure) **Source Location**: `src/omnibase_infra/event_bus/event_bus_kafka.py` @@ -57,7 +57,7 @@ All configuration is managed through environment variables with sensible default | Variable | Default | Range | Description | |----------|---------|-------|-------------| -| `KAFKA_BOOTSTRAP_SERVERS` | `localhost:9092` | - | Comma-separated broker addresses | +| `KAFKA_BOOTSTRAP_SERVERS` | `localhost:9092` | - | Comma-separated broker addresses. **Must be overridden** — per rule #7 localhost defaults are for local development only; per rule #8 Kafka is required infrastructure. | | `KAFKA_ENVIRONMENT` | `local` | - | Environment identifier (e.g., `dev`, `staging`, `prod`) | | `KAFKA_GROUP` | `default` | - | Consumer group identifier | diff --git a/src/omnibase_infra/event_bus/event_bus_kafka.py b/src/omnibase_infra/event_bus/event_bus_kafka.py index 1b4baa8dfd..96cd155b0a 100644 --- a/src/omnibase_infra/event_bus/event_bus_kafka.py +++ b/src/omnibase_infra/event_bus/event_bus_kafka.py @@ -12,7 +12,7 @@ - Circuit breaker for connection failure protection - Retry with exponential backoff on publish failures - Dead letter queue (DLQ) for failed message processing - - Graceful degradation when Kafka is unavailable + - Resilience against transient Kafka broker failures (per platform-wide rule #8) - Support for environment/group-based routing - Proper producer/consumer lifecycle management @@ -220,8 +220,8 @@ class EventBusKafka( Implements ProtocolEventBus interface using Apache Kafka (via aiokafka) with resilience patterns including circuit breaker, retry with exponential - backoff, dead letter queue support, and graceful degradation when Kafka - is unavailable. + backoff, dead letter queue support, and resilience against transient + broker failures (per platform-wide rule #8: Kafka is required infrastructure). Features: - Topic-based message routing with Kafka partitioning @@ -484,12 +484,13 @@ async def start(self) -> None: """Start the event bus and connect to Kafka. Initializes the Kafka producer with connection retry and circuit - breaker protection. If connection fails, the bus operates in - degraded mode where publishes will fail gracefully. + breaker protection. Per platform-wide rule #8, Kafka is required + infrastructure — connection failures raise and must be treated as + fatal by the caller. Raises: - InfraConnectionError: If connection fails after all retries and - circuit breaker is open + InfraTimeoutError: If connection times out + InfraConnectionError: If connection fails after retries """ if self._started: logger.debug("EventBusKafka already started") diff --git a/src/omnibase_infra/runtime/providers/provider_kafka_producer.py b/src/omnibase_infra/runtime/providers/provider_kafka_producer.py index 6b247b275c..610721bfea 100644 --- a/src/omnibase_infra/runtime/providers/provider_kafka_producer.py +++ b/src/omnibase_infra/runtime/providers/provider_kafka_producer.py @@ -3,7 +3,8 @@ """Kafka producer provider. Creates AIOKafkaProducer instances from environment-driven configuration. -Respects architecture constraint #7: Never block on Kafka. +Respects platform-wide rule #8: Kafka is required infrastructure — +use async/non-blocking patterns. Part of OMN-1976: Contract dependency materialization. """ @@ -27,8 +28,9 @@ class ProviderKafkaProducer: Producers are created from KAFKA_* environment variables and shared across all contracts that declare kafka_producer dependencies. - Per architecture constraint #7: Kafka is optional. Creation failures - are logged but do not block startup. + Per platform-wide rule #8: Kafka is required infrastructure. + Creation failures propagate to the caller — callers must treat them + as fatal. Uses async patterns to avoid blocking the calling thread. """ def __init__(self, config: ModelKafkaProducerConfig) -> None: