omnibase_infra 0.2.5__py3-none-any.whl → 0.2.7__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 (139) hide show
  1. omnibase_infra/constants_topic_patterns.py +26 -0
  2. omnibase_infra/enums/__init__.py +3 -0
  3. omnibase_infra/enums/enum_consumer_group_purpose.py +92 -0
  4. omnibase_infra/enums/enum_handler_source_mode.py +16 -2
  5. omnibase_infra/errors/__init__.py +4 -0
  6. omnibase_infra/errors/error_binding_resolution.py +128 -0
  7. omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +0 -2
  8. omnibase_infra/event_bus/event_bus_inmemory.py +64 -10
  9. omnibase_infra/event_bus/event_bus_kafka.py +105 -47
  10. omnibase_infra/event_bus/mixin_kafka_broadcast.py +3 -7
  11. omnibase_infra/event_bus/mixin_kafka_dlq.py +12 -6
  12. omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +0 -81
  13. omnibase_infra/event_bus/testing/__init__.py +26 -0
  14. omnibase_infra/event_bus/testing/adapter_protocol_event_publisher_inmemory.py +418 -0
  15. omnibase_infra/event_bus/testing/model_publisher_metrics.py +64 -0
  16. omnibase_infra/handlers/handler_consul.py +2 -0
  17. omnibase_infra/handlers/mixins/__init__.py +5 -0
  18. omnibase_infra/handlers/mixins/mixin_consul_service.py +274 -10
  19. omnibase_infra/handlers/mixins/mixin_consul_topic_index.py +585 -0
  20. omnibase_infra/handlers/models/model_filesystem_config.py +4 -4
  21. omnibase_infra/migrations/001_create_event_ledger.sql +166 -0
  22. omnibase_infra/migrations/001_drop_event_ledger.sql +18 -0
  23. omnibase_infra/mixins/mixin_node_introspection.py +189 -19
  24. omnibase_infra/models/__init__.py +8 -0
  25. omnibase_infra/models/bindings/__init__.py +59 -0
  26. omnibase_infra/models/bindings/constants.py +144 -0
  27. omnibase_infra/models/bindings/model_binding_resolution_result.py +103 -0
  28. omnibase_infra/models/bindings/model_operation_binding.py +44 -0
  29. omnibase_infra/models/bindings/model_operation_bindings_subcontract.py +152 -0
  30. omnibase_infra/models/bindings/model_parsed_binding.py +52 -0
  31. omnibase_infra/models/discovery/model_introspection_config.py +25 -17
  32. omnibase_infra/models/dispatch/__init__.py +8 -0
  33. omnibase_infra/models/dispatch/model_debug_trace_snapshot.py +114 -0
  34. omnibase_infra/models/dispatch/model_materialized_dispatch.py +141 -0
  35. omnibase_infra/models/handlers/model_handler_source_config.py +1 -1
  36. omnibase_infra/models/model_node_identity.py +126 -0
  37. omnibase_infra/models/projection/model_snapshot_topic_config.py +3 -2
  38. omnibase_infra/models/registration/__init__.py +9 -0
  39. omnibase_infra/models/registration/model_event_bus_topic_entry.py +59 -0
  40. omnibase_infra/models/registration/model_node_event_bus_config.py +99 -0
  41. omnibase_infra/models/registration/model_node_introspection_event.py +11 -0
  42. omnibase_infra/models/runtime/__init__.py +9 -0
  43. omnibase_infra/models/validation/model_coverage_metrics.py +2 -2
  44. omnibase_infra/nodes/__init__.py +9 -0
  45. omnibase_infra/nodes/contract_registry_reducer/__init__.py +29 -0
  46. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +255 -0
  47. omnibase_infra/nodes/contract_registry_reducer/models/__init__.py +38 -0
  48. omnibase_infra/nodes/contract_registry_reducer/models/model_contract_registry_state.py +266 -0
  49. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_cleanup_topic_references.py +55 -0
  50. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_deactivate_contract.py +58 -0
  51. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_mark_stale.py +49 -0
  52. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_heartbeat.py +71 -0
  53. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_topic.py +66 -0
  54. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_upsert_contract.py +92 -0
  55. omnibase_infra/nodes/contract_registry_reducer/node.py +121 -0
  56. omnibase_infra/nodes/contract_registry_reducer/reducer.py +784 -0
  57. omnibase_infra/nodes/contract_registry_reducer/registry/__init__.py +9 -0
  58. omnibase_infra/nodes/contract_registry_reducer/registry/registry_infra_contract_registry_reducer.py +101 -0
  59. omnibase_infra/nodes/handlers/consul/contract.yaml +85 -0
  60. omnibase_infra/nodes/handlers/db/contract.yaml +72 -0
  61. omnibase_infra/nodes/handlers/graph/contract.yaml +127 -0
  62. omnibase_infra/nodes/handlers/http/contract.yaml +74 -0
  63. omnibase_infra/nodes/handlers/intent/contract.yaml +66 -0
  64. omnibase_infra/nodes/handlers/mcp/contract.yaml +69 -0
  65. omnibase_infra/nodes/handlers/vault/contract.yaml +91 -0
  66. omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +50 -0
  67. omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +104 -0
  68. omnibase_infra/nodes/node_ledger_projection_compute/node.py +284 -0
  69. omnibase_infra/nodes/node_ledger_projection_compute/registry/__init__.py +29 -0
  70. omnibase_infra/nodes/node_ledger_projection_compute/registry/registry_infra_ledger_projection.py +118 -0
  71. omnibase_infra/nodes/node_ledger_write_effect/__init__.py +82 -0
  72. omnibase_infra/nodes/node_ledger_write_effect/contract.yaml +200 -0
  73. omnibase_infra/nodes/node_ledger_write_effect/handlers/__init__.py +22 -0
  74. omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_append.py +372 -0
  75. omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_query.py +597 -0
  76. omnibase_infra/nodes/node_ledger_write_effect/models/__init__.py +31 -0
  77. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_append_result.py +54 -0
  78. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_entry.py +92 -0
  79. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query.py +53 -0
  80. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query_result.py +41 -0
  81. omnibase_infra/nodes/node_ledger_write_effect/node.py +89 -0
  82. omnibase_infra/nodes/node_ledger_write_effect/protocols/__init__.py +13 -0
  83. omnibase_infra/nodes/node_ledger_write_effect/protocols/protocol_ledger_persistence.py +127 -0
  84. omnibase_infra/nodes/node_ledger_write_effect/registry/__init__.py +9 -0
  85. omnibase_infra/nodes/node_ledger_write_effect/registry/registry_infra_ledger_write.py +121 -0
  86. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +7 -5
  87. omnibase_infra/nodes/reducers/models/__init__.py +7 -2
  88. omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +11 -0
  89. omnibase_infra/nodes/reducers/models/model_payload_ledger_append.py +133 -0
  90. omnibase_infra/nodes/reducers/registration_reducer.py +1 -0
  91. omnibase_infra/protocols/__init__.py +3 -0
  92. omnibase_infra/protocols/protocol_dispatch_engine.py +152 -0
  93. omnibase_infra/runtime/__init__.py +60 -0
  94. omnibase_infra/runtime/binding_resolver.py +753 -0
  95. omnibase_infra/runtime/constants_security.py +70 -0
  96. omnibase_infra/runtime/contract_loaders/__init__.py +9 -0
  97. omnibase_infra/runtime/contract_loaders/operation_bindings_loader.py +789 -0
  98. omnibase_infra/runtime/emit_daemon/__init__.py +97 -0
  99. omnibase_infra/runtime/emit_daemon/cli.py +844 -0
  100. omnibase_infra/runtime/emit_daemon/client.py +811 -0
  101. omnibase_infra/runtime/emit_daemon/config.py +535 -0
  102. omnibase_infra/runtime/emit_daemon/daemon.py +812 -0
  103. omnibase_infra/runtime/emit_daemon/event_registry.py +477 -0
  104. omnibase_infra/runtime/emit_daemon/model_daemon_request.py +139 -0
  105. omnibase_infra/runtime/emit_daemon/model_daemon_response.py +191 -0
  106. omnibase_infra/runtime/emit_daemon/queue.py +618 -0
  107. omnibase_infra/runtime/event_bus_subcontract_wiring.py +466 -0
  108. omnibase_infra/runtime/handler_source_resolver.py +43 -2
  109. omnibase_infra/runtime/kafka_contract_source.py +984 -0
  110. omnibase_infra/runtime/models/__init__.py +13 -0
  111. omnibase_infra/runtime/models/model_contract_load_result.py +224 -0
  112. omnibase_infra/runtime/models/model_runtime_contract_config.py +268 -0
  113. omnibase_infra/runtime/models/model_runtime_scheduler_config.py +4 -3
  114. omnibase_infra/runtime/models/model_security_config.py +109 -0
  115. omnibase_infra/runtime/publisher_topic_scoped.py +294 -0
  116. omnibase_infra/runtime/runtime_contract_config_loader.py +406 -0
  117. omnibase_infra/runtime/service_kernel.py +76 -6
  118. omnibase_infra/runtime/service_message_dispatch_engine.py +558 -15
  119. omnibase_infra/runtime/service_runtime_host_process.py +770 -20
  120. omnibase_infra/runtime/transition_notification_publisher.py +3 -2
  121. omnibase_infra/runtime/util_wiring.py +206 -62
  122. omnibase_infra/services/mcp/service_mcp_tool_sync.py +27 -9
  123. omnibase_infra/services/session/config_consumer.py +25 -8
  124. omnibase_infra/services/session/config_store.py +2 -2
  125. omnibase_infra/services/session/consumer.py +1 -1
  126. omnibase_infra/topics/__init__.py +45 -0
  127. omnibase_infra/topics/platform_topic_suffixes.py +140 -0
  128. omnibase_infra/topics/util_topic_composition.py +95 -0
  129. omnibase_infra/types/typed_dict/__init__.py +9 -1
  130. omnibase_infra/types/typed_dict/typed_dict_envelope_build_params.py +115 -0
  131. omnibase_infra/utils/__init__.py +9 -0
  132. omnibase_infra/utils/util_consumer_group.py +232 -0
  133. omnibase_infra/validation/infra_validators.py +18 -1
  134. omnibase_infra/validation/validation_exemptions.yaml +192 -0
  135. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/METADATA +3 -3
  136. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/RECORD +139 -52
  137. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/entry_points.txt +1 -0
  138. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/WHEEL +0 -0
  139. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,200 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 OmniNode Team
3
+ #
4
+ # ONEX Node Contract
5
+ # Node: NodeLedgerWriteEffect
6
+ #
7
+ # Capability-oriented effect node for event ledger write operations.
8
+ # Provides append-only audit ledger storage with idempotent writes.
9
+ #
10
+ # This contract defines the interface for storing events to the audit ledger
11
+ # and querying events by various criteria. The ledger uses PostgreSQL with
12
+ # a (topic, partition, kafka_offset) unique constraint for idempotency.
13
+ #
14
+ # Related Tickets:
15
+ # - OMN-1646: Event Ledger Schema and Models
16
+ # - OMN-1647: Ledger Write Effect Node
17
+ # Contract identifiers
18
+ name: "node_ledger_write_effect"
19
+ contract_name: "node_ledger_write_effect"
20
+ node_name: "node_ledger_write_effect"
21
+ contract_version:
22
+ major: 0
23
+ minor: 1
24
+ patch: 0
25
+ node_version:
26
+ major: 0
27
+ minor: 1
28
+ patch: 0
29
+ # Node type
30
+ node_type: "EFFECT_GENERIC"
31
+ # Description
32
+ description: >
33
+ Capability-oriented effect node for event ledger write operations. Handles appending events to the PostgreSQL audit ledger with idempotent write support via unique constraint on (topic, partition, kafka_offset). Supports querying events by correlation ID, event type, topic, or time range.
34
+
35
+ # Strongly typed I/O models
36
+ input_model:
37
+ name: "ModelPayloadLedgerAppend"
38
+ module: "omnibase_infra.nodes.reducers.models"
39
+ description: "Input model for ledger append operations (event payload with Kafka position)."
40
+ output_model:
41
+ name: "ModelLedgerAppendResult"
42
+ module: "omnibase_infra.nodes.node_ledger_write_effect.models"
43
+ description: "Output model for ledger append results (success, entry ID, duplicate flag)."
44
+ # Capability declaration (capability-oriented naming)
45
+ capabilities:
46
+ - name: "ledger.write"
47
+ description: "Append events to the audit ledger with idempotent write support"
48
+ version: "0.1.0"
49
+ - name: "ledger.write.append"
50
+ description: "Append a single event to the ledger"
51
+ - name: "ledger.query"
52
+ description: "Query events from the audit ledger"
53
+ - name: "ledger.query.by_correlation"
54
+ description: "Query events by correlation ID"
55
+ - name: "ledger.query.by_topic"
56
+ description: "Query events by Kafka topic"
57
+ - name: "ledger.query.by_time_range"
58
+ description: "Query events within a time range"
59
+ # Event-driven topic configuration
60
+ # Uses {env}.{namespace} placeholders for environment-aware topics
61
+ consumed_events:
62
+ - topic: "{env}.{namespace}.onex.cmd.ledger-append.v1"
63
+ event_type: "LedgerAppendCommand"
64
+ message_category: "COMMAND"
65
+ description: "Command to append an event to the audit ledger"
66
+ - topic: "{env}.{namespace}.onex.cmd.ledger-query.v1"
67
+ event_type: "LedgerQueryCommand"
68
+ message_category: "COMMAND"
69
+ description: "Command to query events from the audit ledger"
70
+ published_events:
71
+ - topic: "{env}.{namespace}.onex.evt.ledger-appended.v1"
72
+ event_type: "LedgerAppendedEvent"
73
+ description: "Confirmation that event was appended to the ledger"
74
+ - topic: "{env}.{namespace}.onex.evt.ledger-query-result.v1"
75
+ event_type: "LedgerQueryResultEvent"
76
+ description: "Result of a ledger query operation"
77
+ # IO operations (EFFECT node specific)
78
+ # Note: correlation_id is optional (UUID | None) - audit ledger must never drop events.
79
+ # Provide correlation_id for distributed tracing when available.
80
+ io_operations:
81
+ - operation: "ledger.append"
82
+ description: "Append an event to the audit ledger with idempotent write support"
83
+ input_fields:
84
+ - topic: "str"
85
+ - partition: "int"
86
+ - kafka_offset: "int"
87
+ - event_key: "str | None"
88
+ - event_value: "str"
89
+ - onex_headers: "dict[str, object]"
90
+ - correlation_id: "UUID | None"
91
+ - envelope_id: "UUID | None"
92
+ - event_type: "str | None"
93
+ - source: "str | None"
94
+ - event_timestamp: "datetime | None"
95
+ output_fields:
96
+ - success: "bool"
97
+ - ledger_entry_id: "UUID | None"
98
+ - duplicate: "bool"
99
+ - topic: "str"
100
+ - partition: "int"
101
+ - kafka_offset: "int"
102
+ # Idempotent: Uses ON CONFLICT DO NOTHING with (topic, partition, kafka_offset).
103
+ # Duplicate calls return duplicate=True without creating new entries.
104
+ idempotent: true
105
+ - operation: "ledger.query"
106
+ description: "Query events from the audit ledger by various criteria"
107
+ input_fields:
108
+ - correlation_id: "UUID | None"
109
+ - event_type: "str | None"
110
+ - topic: "str | None"
111
+ - start_time: "datetime | None"
112
+ - end_time: "datetime | None"
113
+ - limit: "int"
114
+ - offset: "int"
115
+ output_fields:
116
+ - entries: "list[ModelLedgerEntry]"
117
+ - total_count: "int"
118
+ - has_more: "bool"
119
+ idempotent: true
120
+ # Handler routing for ledger operations
121
+ # Handlers compose with HandlerDb for PostgreSQL operations
122
+ handler_routing:
123
+ routing_strategy: "operation_match"
124
+ handlers:
125
+ - operation: "ledger.append"
126
+ handler_class: "HandlerLedgerAppend"
127
+ handler_module: "omnibase_infra.nodes.node_ledger_write_effect.handlers.handler_ledger_append"
128
+ description: "Idempotent append to event ledger with duplicate detection"
129
+ - operation: "ledger.query"
130
+ handler_class: "HandlerLedgerQuery"
131
+ handler_module: "omnibase_infra.nodes.node_ledger_write_effect.handlers.handler_ledger_query"
132
+ description: "Query ledger by correlation_id, time_range, event_type, topic"
133
+ # Dependencies (protocols this node requires)
134
+ # Note: Ledger handlers compose with HandlerDb internally
135
+ dependencies:
136
+ - name: "handler_ledger_append"
137
+ type: "handler"
138
+ class_name: "HandlerLedgerAppend"
139
+ module: "omnibase_infra.nodes.node_ledger_write_effect.handlers.handler_ledger_append"
140
+ description: "Handler for idempotent ledger append operations"
141
+ - name: "handler_ledger_query"
142
+ type: "handler"
143
+ class_name: "HandlerLedgerQuery"
144
+ module: "omnibase_infra.nodes.node_ledger_write_effect.handlers.handler_ledger_query"
145
+ description: "Handler for ledger query operations"
146
+ - name: "handler_db"
147
+ type: "handler"
148
+ class_name: "HandlerDb"
149
+ module: "omnibase_infra.handlers.handler_db"
150
+ description: "PostgreSQL database handler (composed by ledger handlers)"
151
+ # Error handling configuration
152
+ error_handling:
153
+ retry_policy:
154
+ max_retries: 3
155
+ initial_delay_ms: 100
156
+ max_delay_ms: 5000
157
+ exponential_base: 2
158
+ retry_on:
159
+ - "InfraConnectionError"
160
+ - "InfraTimeoutError"
161
+ circuit_breaker:
162
+ enabled: true
163
+ failure_threshold: 5
164
+ reset_timeout_ms: 60000
165
+ error_types:
166
+ - name: "DatabaseConnectionError"
167
+ description: "Unable to connect to PostgreSQL"
168
+ recoverable: true
169
+ retry_strategy: "exponential_backoff"
170
+ - name: "DatabaseTimeoutError"
171
+ description: "Database operation timed out"
172
+ recoverable: true
173
+ retry_strategy: "exponential_backoff"
174
+ - name: "ValidationError"
175
+ description: "Input validation failed"
176
+ recoverable: false
177
+ retry_strategy: "none"
178
+ # Health check configuration
179
+ health_check:
180
+ enabled: true
181
+ endpoint: "/health"
182
+ interval_seconds: 30
183
+ timeout_ms: 5000
184
+ checks:
185
+ - name: "database_backend"
186
+ type: "connection"
187
+ critical: true
188
+ # Metadata
189
+ metadata:
190
+ author: "OmniNode Team"
191
+ license: "MIT"
192
+ created: "2026-01-29"
193
+ tags:
194
+ - effect
195
+ - ledger
196
+ - audit
197
+ - storage
198
+ - postgresql
199
+ - capability-oriented
200
+ - idempotent
@@ -0,0 +1,22 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 OmniNode Team
3
+ """Handlers for event ledger persistence operations.
4
+
5
+ This package provides handlers for the event ledger effect node:
6
+ - HandlerLedgerAppend: Idempotent INSERT with duplicate detection
7
+ - HandlerLedgerQuery: Query by correlation_id, time_range, etc.
8
+
9
+ Both handlers compose with HandlerDb for PostgreSQL operations.
10
+ """
11
+
12
+ from omnibase_infra.nodes.node_ledger_write_effect.handlers.handler_ledger_append import (
13
+ HandlerLedgerAppend,
14
+ )
15
+ from omnibase_infra.nodes.node_ledger_write_effect.handlers.handler_ledger_query import (
16
+ HandlerLedgerQuery,
17
+ )
18
+
19
+ __all__ = [
20
+ "HandlerLedgerAppend",
21
+ "HandlerLedgerQuery",
22
+ ]
@@ -0,0 +1,372 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 OmniNode Team
3
+ """Handler for ledger append operations with idempotent write support.
4
+
5
+ This handler composes with HandlerDb for PostgreSQL operations, providing
6
+ a typed interface for appending events to the audit ledger with duplicate
7
+ detection via ON CONFLICT DO NOTHING.
8
+
9
+ Bytes Encoding:
10
+ The ModelPayloadLedgerAppend contains base64-encoded event_key and event_value
11
+ since bytes cannot safely cross intent boundaries. This handler decodes them
12
+ to bytes before passing to PostgreSQL, which stores them as BYTEA.
13
+
14
+ Idempotency:
15
+ Uses INSERT ... ON CONFLICT (topic, partition, kafka_offset) DO NOTHING RETURNING.
16
+ If RETURNING returns no rows, the event was already in the ledger (duplicate).
17
+ Duplicates are not errors - they enable idempotent replay.
18
+
19
+ Design Decision - Composition with HandlerDb:
20
+ This handler delegates SQL execution to HandlerDb rather than using asyncpg
21
+ directly. This provides:
22
+ - Circuit breaker protection
23
+ - Error classification (transient vs permanent)
24
+ - Connection pool management
25
+ - Consistent error handling
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import base64
31
+ import json
32
+ import logging
33
+ from typing import TYPE_CHECKING
34
+ from uuid import UUID, uuid4
35
+
36
+ from omnibase_core.models.dispatch import ModelHandlerOutput
37
+ from omnibase_infra.enums import (
38
+ EnumHandlerType,
39
+ EnumHandlerTypeCategory,
40
+ EnumInfraTransportType,
41
+ EnumResponseStatus,
42
+ )
43
+ from omnibase_infra.errors import ModelInfraErrorContext, RuntimeHostError
44
+ from omnibase_infra.nodes.node_ledger_write_effect.models import ModelLedgerAppendResult
45
+
46
+ if TYPE_CHECKING:
47
+ from omnibase_core.container import ModelONEXContainer
48
+ from omnibase_infra.handlers.handler_db import HandlerDb
49
+ from omnibase_infra.nodes.reducers.models import ModelPayloadLedgerAppend
50
+
51
+ logger = logging.getLogger(__name__)
52
+
53
+ # Handler ID for ModelHandlerOutput
54
+ HANDLER_ID_LEDGER_APPEND: str = "ledger-append-handler"
55
+
56
+ # SQL for idempotent append with duplicate detection
57
+ # Uses RETURNING to detect whether insert succeeded (returns row) or
58
+ # ON CONFLICT was triggered (returns nothing)
59
+ _SQL_APPEND = """
60
+ INSERT INTO event_ledger (
61
+ topic,
62
+ partition,
63
+ kafka_offset,
64
+ event_key,
65
+ event_value,
66
+ onex_headers,
67
+ envelope_id,
68
+ correlation_id,
69
+ event_type,
70
+ source,
71
+ event_timestamp
72
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
73
+ ON CONFLICT (topic, partition, kafka_offset) DO NOTHING
74
+ RETURNING ledger_entry_id
75
+ """
76
+
77
+
78
+ class HandlerLedgerAppend:
79
+ """Handler for appending events to the audit ledger with idempotent writes.
80
+
81
+ This handler implements the append operation for ProtocolLedgerPersistence,
82
+ composing with HandlerDb for PostgreSQL operations. It provides:
83
+
84
+ - Base64 decoding of event payloads to bytes
85
+ - Idempotent INSERT via ON CONFLICT DO NOTHING
86
+ - Duplicate detection via RETURNING clause
87
+ - Type-safe input/output with Pydantic models
88
+
89
+ Attributes:
90
+ handler_type: EnumHandlerType.INFRA_HANDLER
91
+ handler_category: EnumHandlerTypeCategory.EFFECT
92
+
93
+ Example:
94
+ >>> handler = HandlerLedgerAppend(container, db_handler)
95
+ >>> await handler.initialize({})
96
+ >>> result = await handler.append(payload)
97
+ >>> if result.duplicate:
98
+ ... logger.info("Event already in ledger")
99
+ """
100
+
101
+ def __init__(
102
+ self,
103
+ container: ModelONEXContainer,
104
+ db_handler: HandlerDb,
105
+ ) -> None:
106
+ """Initialize the ledger append handler.
107
+
108
+ Args:
109
+ container: ONEX dependency injection container.
110
+ db_handler: Initialized HandlerDb instance for PostgreSQL operations.
111
+ """
112
+ self._container = container
113
+ self._db_handler = db_handler
114
+ self._initialized: bool = False
115
+
116
+ @property
117
+ def handler_type(self) -> EnumHandlerType:
118
+ """Return the architectural role of this handler."""
119
+ return EnumHandlerType.INFRA_HANDLER
120
+
121
+ @property
122
+ def handler_category(self) -> EnumHandlerTypeCategory:
123
+ """Return the behavioral classification of this handler."""
124
+ return EnumHandlerTypeCategory.EFFECT
125
+
126
+ async def initialize(self, config: dict[str, object]) -> None:
127
+ """Initialize the handler.
128
+
129
+ The underlying HandlerDb must already be initialized before
130
+ calling this method.
131
+
132
+ Args:
133
+ config: Configuration dict (currently unused).
134
+
135
+ Raises:
136
+ RuntimeHostError: If HandlerDb is not initialized.
137
+ """
138
+ # Verify db_handler is initialized
139
+ if not getattr(self._db_handler, "_initialized", False):
140
+ ctx = ModelInfraErrorContext.with_correlation(
141
+ transport_type=EnumInfraTransportType.DATABASE,
142
+ operation="initialize",
143
+ )
144
+ raise RuntimeHostError(
145
+ "HandlerDb must be initialized before HandlerLedgerAppend",
146
+ context=ctx,
147
+ )
148
+
149
+ self._initialized = True
150
+ logger.info(
151
+ "%s initialized successfully",
152
+ self.__class__.__name__,
153
+ extra={"handler": self.__class__.__name__},
154
+ )
155
+
156
+ async def shutdown(self) -> None:
157
+ """Shutdown the handler.
158
+
159
+ Does not shutdown the underlying HandlerDb - that is managed separately.
160
+ """
161
+ self._initialized = False
162
+ logger.info("HandlerLedgerAppend shutdown complete")
163
+
164
+ async def append(
165
+ self,
166
+ payload: ModelPayloadLedgerAppend,
167
+ ) -> ModelLedgerAppendResult:
168
+ """Append an event to the audit ledger.
169
+
170
+ Decodes base64 event data, executes idempotent INSERT, and detects
171
+ duplicates via the RETURNING clause.
172
+
173
+ Args:
174
+ payload: Event payload containing Kafka position and event data.
175
+
176
+ Returns:
177
+ ModelLedgerAppendResult with success, ledger_entry_id, and duplicate flag.
178
+
179
+ Raises:
180
+ RuntimeHostError: If handler not initialized or validation fails.
181
+ InfraConnectionError: If database connection fails.
182
+ InfraTimeoutError: If operation times out.
183
+ """
184
+ correlation_id = payload.correlation_id or uuid4()
185
+
186
+ if not self._initialized:
187
+ ctx = ModelInfraErrorContext.with_correlation(
188
+ correlation_id=correlation_id,
189
+ transport_type=EnumInfraTransportType.DATABASE,
190
+ operation="ledger.append",
191
+ )
192
+ raise RuntimeHostError(
193
+ "HandlerLedgerAppend not initialized. Call initialize() first.",
194
+ context=ctx,
195
+ )
196
+
197
+ # Decode base64 event data to bytes
198
+ event_key_bytes = (
199
+ self._decode_base64(payload.event_key) if payload.event_key else None
200
+ )
201
+ event_value_bytes = self._decode_base64(payload.event_value)
202
+
203
+ # Serialize onex_headers to JSON string for JSONB column
204
+ onex_headers_json = json.dumps(payload.onex_headers)
205
+
206
+ # Build parameters for INSERT
207
+ # Order must match $1..$11 in _SQL_APPEND
208
+ parameters: list[object] = [
209
+ payload.topic, # $1
210
+ payload.partition, # $2
211
+ payload.kafka_offset, # $3
212
+ event_key_bytes, # $4 (BYTEA, nullable)
213
+ event_value_bytes, # $5 (BYTEA)
214
+ onex_headers_json, # $6 (JSONB)
215
+ str(payload.envelope_id)
216
+ if payload.envelope_id
217
+ else None, # $7 (UUID, nullable)
218
+ str(payload.correlation_id)
219
+ if payload.correlation_id
220
+ else None, # $8 (UUID, nullable)
221
+ payload.event_type, # $9 (TEXT, nullable)
222
+ payload.source, # $10 (TEXT, nullable)
223
+ payload.event_timestamp, # $11 (TIMESTAMPTZ, nullable)
224
+ ]
225
+
226
+ # Build envelope for HandlerDb
227
+ envelope: dict[str, object] = {
228
+ "operation": "db.query", # Use query because RETURNING produces rows
229
+ "payload": {
230
+ "sql": _SQL_APPEND,
231
+ "parameters": parameters,
232
+ },
233
+ "correlation_id": str(correlation_id),
234
+ }
235
+
236
+ logger.debug(
237
+ "Appending event to ledger",
238
+ extra={
239
+ "topic": payload.topic,
240
+ "partition": payload.partition,
241
+ "offset": payload.kafka_offset,
242
+ "correlation_id": str(correlation_id),
243
+ },
244
+ )
245
+
246
+ # Execute via HandlerDb
247
+ db_result = await self._db_handler.execute(envelope)
248
+
249
+ # Check if RETURNING produced a row (insert succeeded) or not (duplicate)
250
+ # db_result.result is guaranteed non-None for successful db operations
251
+ if db_result.result is None:
252
+ ctx = ModelInfraErrorContext.with_correlation(
253
+ correlation_id=correlation_id,
254
+ transport_type=EnumInfraTransportType.DATABASE,
255
+ operation="ledger.append",
256
+ )
257
+ raise RuntimeHostError("Database operation returned no result", context=ctx)
258
+
259
+ rows = db_result.result.payload.rows
260
+ if rows and len(rows) > 0:
261
+ # Insert succeeded - extract ledger_entry_id from RETURNING
262
+ ledger_entry_id = UUID(str(rows[0]["ledger_entry_id"]))
263
+ duplicate = False
264
+ logger.debug(
265
+ "Event appended to ledger",
266
+ extra={
267
+ "ledger_entry_id": str(ledger_entry_id),
268
+ "topic": payload.topic,
269
+ "partition": payload.partition,
270
+ "offset": payload.kafka_offset,
271
+ },
272
+ )
273
+ else:
274
+ # ON CONFLICT DO NOTHING triggered - duplicate
275
+ ledger_entry_id = None
276
+ duplicate = True
277
+ logger.debug(
278
+ "Duplicate event detected (already in ledger)",
279
+ extra={
280
+ "topic": payload.topic,
281
+ "partition": payload.partition,
282
+ "offset": payload.kafka_offset,
283
+ },
284
+ )
285
+
286
+ return ModelLedgerAppendResult(
287
+ success=True,
288
+ ledger_entry_id=ledger_entry_id,
289
+ duplicate=duplicate,
290
+ topic=payload.topic,
291
+ partition=payload.partition,
292
+ kafka_offset=payload.kafka_offset,
293
+ )
294
+
295
+ def _decode_base64(self, encoded: str) -> bytes:
296
+ """Decode base64 string to bytes.
297
+
298
+ Args:
299
+ encoded: Base64-encoded string.
300
+
301
+ Returns:
302
+ Decoded bytes.
303
+
304
+ Raises:
305
+ RuntimeHostError: If decoding fails.
306
+ """
307
+ try:
308
+ return base64.b64decode(encoded)
309
+ except Exception as e:
310
+ ctx = ModelInfraErrorContext.with_correlation(
311
+ transport_type=EnumInfraTransportType.DATABASE,
312
+ operation="ledger.append",
313
+ )
314
+ raise RuntimeHostError(
315
+ f"Failed to decode base64 event data: {type(e).__name__}",
316
+ context=ctx,
317
+ ) from e
318
+
319
+ async def execute(
320
+ self,
321
+ envelope: dict[str, object],
322
+ ) -> ModelHandlerOutput[ModelLedgerAppendResult]:
323
+ """Execute ledger append from envelope (ProtocolHandler interface).
324
+
325
+ This method provides the standard handler interface for contract-driven
326
+ invocation. It extracts the payload from the envelope and delegates to
327
+ the append() method.
328
+
329
+ Args:
330
+ envelope: Request envelope containing:
331
+ - operation: "ledger.append"
332
+ - payload: ModelPayloadLedgerAppend as dict
333
+ - correlation_id: Optional correlation ID
334
+
335
+ Returns:
336
+ ModelHandlerOutput wrapping ModelLedgerAppendResult.
337
+ """
338
+ from omnibase_infra.nodes.reducers.models import ModelPayloadLedgerAppend
339
+
340
+ correlation_id_raw = envelope.get("correlation_id")
341
+ correlation_id = (
342
+ UUID(str(correlation_id_raw)) if correlation_id_raw else uuid4()
343
+ )
344
+ input_envelope_id = uuid4()
345
+
346
+ payload_raw = envelope.get("payload")
347
+ if not isinstance(payload_raw, dict):
348
+ ctx = ModelInfraErrorContext.with_correlation(
349
+ correlation_id=correlation_id,
350
+ transport_type=EnumInfraTransportType.DATABASE,
351
+ operation="ledger.append",
352
+ )
353
+ raise RuntimeHostError(
354
+ "Missing or invalid 'payload' in envelope",
355
+ context=ctx,
356
+ )
357
+
358
+ # Parse payload into typed model
359
+ payload = ModelPayloadLedgerAppend.model_validate(payload_raw)
360
+
361
+ # Execute append
362
+ result = await self.append(payload)
363
+
364
+ return ModelHandlerOutput.for_compute(
365
+ input_envelope_id=input_envelope_id,
366
+ correlation_id=correlation_id,
367
+ handler_id=HANDLER_ID_LEDGER_APPEND,
368
+ result=result,
369
+ )
370
+
371
+
372
+ __all__ = ["HandlerLedgerAppend"]