omnibase_infra 0.3.1__py3-none-any.whl → 0.4.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 (117) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/enums/__init__.py +3 -0
  3. omnibase_infra/enums/enum_consumer_group_purpose.py +9 -0
  4. omnibase_infra/enums/enum_postgres_error_code.py +188 -0
  5. omnibase_infra/errors/__init__.py +4 -0
  6. omnibase_infra/errors/error_infra.py +60 -0
  7. omnibase_infra/handlers/__init__.py +3 -0
  8. omnibase_infra/handlers/handler_slack_webhook.py +426 -0
  9. omnibase_infra/handlers/models/__init__.py +14 -0
  10. omnibase_infra/handlers/models/enum_alert_severity.py +36 -0
  11. omnibase_infra/handlers/models/model_slack_alert.py +24 -0
  12. omnibase_infra/handlers/models/model_slack_alert_payload.py +77 -0
  13. omnibase_infra/handlers/models/model_slack_alert_result.py +73 -0
  14. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +29 -20
  15. omnibase_infra/mixins/__init__.py +14 -0
  16. omnibase_infra/mixins/mixin_node_introspection.py +42 -20
  17. omnibase_infra/mixins/mixin_postgres_error_response.py +314 -0
  18. omnibase_infra/mixins/mixin_postgres_op_executor.py +298 -0
  19. omnibase_infra/models/__init__.py +3 -0
  20. omnibase_infra/models/discovery/model_dependency_spec.py +1 -0
  21. omnibase_infra/models/discovery/model_discovered_capabilities.py +1 -1
  22. omnibase_infra/models/discovery/model_introspection_config.py +28 -1
  23. omnibase_infra/models/discovery/model_introspection_performance_metrics.py +1 -0
  24. omnibase_infra/models/discovery/model_introspection_task_config.py +1 -0
  25. omnibase_infra/{nodes/effects/models → models}/model_backend_result.py +22 -6
  26. omnibase_infra/models/projection/__init__.py +11 -0
  27. omnibase_infra/models/projection/model_contract_projection.py +170 -0
  28. omnibase_infra/models/projection/model_topic_projection.py +148 -0
  29. omnibase_infra/models/runtime/__init__.py +4 -0
  30. omnibase_infra/models/runtime/model_resolved_dependencies.py +116 -0
  31. omnibase_infra/nodes/contract_registry_reducer/__init__.py +5 -0
  32. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +6 -5
  33. omnibase_infra/nodes/contract_registry_reducer/contract_registration_event_router.py +689 -0
  34. omnibase_infra/nodes/contract_registry_reducer/reducer.py +9 -26
  35. omnibase_infra/nodes/effects/__init__.py +1 -1
  36. omnibase_infra/nodes/effects/models/__init__.py +6 -4
  37. omnibase_infra/nodes/effects/models/model_registry_response.py +1 -1
  38. omnibase_infra/nodes/effects/protocol_consul_client.py +1 -1
  39. omnibase_infra/nodes/effects/protocol_postgres_adapter.py +1 -1
  40. omnibase_infra/nodes/effects/registry_effect.py +1 -1
  41. omnibase_infra/nodes/node_contract_persistence_effect/__init__.py +101 -0
  42. omnibase_infra/nodes/node_contract_persistence_effect/contract.yaml +490 -0
  43. omnibase_infra/nodes/node_contract_persistence_effect/handlers/__init__.py +74 -0
  44. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_cleanup_topics.py +217 -0
  45. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_contract_upsert.py +242 -0
  46. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_deactivate.py +194 -0
  47. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_heartbeat.py +243 -0
  48. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_mark_stale.py +208 -0
  49. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_topic_update.py +298 -0
  50. omnibase_infra/nodes/node_contract_persistence_effect/models/__init__.py +15 -0
  51. omnibase_infra/nodes/node_contract_persistence_effect/models/model_persistence_result.py +52 -0
  52. omnibase_infra/nodes/node_contract_persistence_effect/node.py +131 -0
  53. omnibase_infra/nodes/node_contract_persistence_effect/registry/__init__.py +27 -0
  54. omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +251 -0
  55. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +8 -12
  56. omnibase_infra/nodes/node_registry_effect/models/__init__.py +2 -2
  57. omnibase_infra/nodes/node_slack_alerter_effect/__init__.py +33 -0
  58. omnibase_infra/nodes/node_slack_alerter_effect/contract.yaml +291 -0
  59. omnibase_infra/nodes/node_slack_alerter_effect/node.py +106 -0
  60. omnibase_infra/projectors/__init__.py +6 -0
  61. omnibase_infra/projectors/projection_reader_contract.py +1301 -0
  62. omnibase_infra/runtime/__init__.py +12 -0
  63. omnibase_infra/runtime/baseline_subscriptions.py +13 -6
  64. omnibase_infra/runtime/contract_dependency_resolver.py +455 -0
  65. omnibase_infra/runtime/contract_registration_event_router.py +500 -0
  66. omnibase_infra/runtime/db/__init__.py +4 -0
  67. omnibase_infra/runtime/db/models/__init__.py +15 -10
  68. omnibase_infra/runtime/db/models/model_db_operation.py +40 -0
  69. omnibase_infra/runtime/db/models/model_db_param.py +24 -0
  70. omnibase_infra/runtime/db/models/model_db_repository_contract.py +40 -0
  71. omnibase_infra/runtime/db/models/model_db_return.py +26 -0
  72. omnibase_infra/runtime/db/models/model_db_safety_policy.py +32 -0
  73. omnibase_infra/runtime/emit_daemon/event_registry.py +34 -22
  74. omnibase_infra/runtime/event_bus_subcontract_wiring.py +63 -23
  75. omnibase_infra/runtime/intent_execution_router.py +430 -0
  76. omnibase_infra/runtime/models/__init__.py +6 -0
  77. omnibase_infra/runtime/models/model_contract_registry_config.py +41 -0
  78. omnibase_infra/runtime/models/model_intent_execution_summary.py +79 -0
  79. omnibase_infra/runtime/models/model_runtime_config.py +8 -0
  80. omnibase_infra/runtime/protocols/__init__.py +16 -0
  81. omnibase_infra/runtime/protocols/protocol_intent_executor.py +107 -0
  82. omnibase_infra/runtime/publisher_topic_scoped.py +16 -11
  83. omnibase_infra/runtime/registry_policy.py +29 -15
  84. omnibase_infra/runtime/request_response_wiring.py +793 -0
  85. omnibase_infra/runtime/service_kernel.py +295 -8
  86. omnibase_infra/runtime/service_runtime_host_process.py +149 -5
  87. omnibase_infra/runtime/util_version.py +5 -1
  88. omnibase_infra/schemas/schema_latency_baseline.sql +135 -0
  89. omnibase_infra/services/contract_publisher/config.py +4 -4
  90. omnibase_infra/services/contract_publisher/service.py +8 -5
  91. omnibase_infra/services/observability/injection_effectiveness/__init__.py +67 -0
  92. omnibase_infra/services/observability/injection_effectiveness/config.py +295 -0
  93. omnibase_infra/services/observability/injection_effectiveness/consumer.py +1461 -0
  94. omnibase_infra/services/observability/injection_effectiveness/models/__init__.py +32 -0
  95. omnibase_infra/services/observability/injection_effectiveness/models/model_agent_match.py +79 -0
  96. omnibase_infra/services/observability/injection_effectiveness/models/model_context_utilization.py +118 -0
  97. omnibase_infra/services/observability/injection_effectiveness/models/model_latency_breakdown.py +107 -0
  98. omnibase_infra/services/observability/injection_effectiveness/models/model_pattern_utilization.py +46 -0
  99. omnibase_infra/services/observability/injection_effectiveness/writer_postgres.py +596 -0
  100. omnibase_infra/services/registry_api/models/__init__.py +25 -0
  101. omnibase_infra/services/registry_api/models/model_contract_ref.py +44 -0
  102. omnibase_infra/services/registry_api/models/model_contract_view.py +81 -0
  103. omnibase_infra/services/registry_api/models/model_response_contracts.py +50 -0
  104. omnibase_infra/services/registry_api/models/model_response_topics.py +50 -0
  105. omnibase_infra/services/registry_api/models/model_topic_summary.py +57 -0
  106. omnibase_infra/services/registry_api/models/model_topic_view.py +63 -0
  107. omnibase_infra/services/registry_api/routes.py +205 -6
  108. omnibase_infra/services/registry_api/service.py +528 -1
  109. omnibase_infra/utils/__init__.py +7 -0
  110. omnibase_infra/utils/util_db_error_context.py +292 -0
  111. omnibase_infra/validation/infra_validators.py +3 -1
  112. omnibase_infra/validation/validation_exemptions.yaml +65 -0
  113. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/METADATA +3 -3
  114. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/RECORD +117 -58
  115. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/WHEEL +0 -0
  116. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/entry_points.txt +0 -0
  117. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -50,6 +50,7 @@ import asyncpg
50
50
 
51
51
  from omnibase_core.container import ModelONEXContainer
52
52
  from omnibase_core.enums.enum_node_kind import EnumNodeKind
53
+ from omnibase_core.models.primitives.model_semver import ModelSemVer
53
54
  from omnibase_infra.enums import EnumInfraTransportType
54
55
  from omnibase_infra.errors import (
55
56
  InfraConnectionError,
@@ -87,6 +88,9 @@ DEFAULT_POOL_SIZE = 10
87
88
  DEFAULT_TIMEOUT_SECONDS = 30.0
88
89
 
89
90
  # SQL statements
91
+ # NOTE: Database column is `registered_at` but model uses `created_at`. The column
92
+ # is aliased in queries for mapping. This aligns with the existing database schema
93
+ # on 192.168.86.200 which uses `registered_at` for the creation timestamp.
90
94
  SQL_CREATE_TABLE = """
91
95
  CREATE TABLE IF NOT EXISTS node_registrations (
92
96
  node_id UUID PRIMARY KEY,
@@ -95,13 +99,13 @@ CREATE TABLE IF NOT EXISTS node_registrations (
95
99
  capabilities JSONB NOT NULL DEFAULT '[]',
96
100
  endpoints JSONB NOT NULL DEFAULT '{}',
97
101
  metadata JSONB NOT NULL DEFAULT '{}',
98
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
102
+ registered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
99
103
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
100
104
  );
101
105
  """
102
106
 
103
107
  SQL_UPSERT = """
104
- INSERT INTO node_registrations (node_id, node_type, node_version, capabilities, endpoints, metadata, created_at, updated_at)
108
+ INSERT INTO node_registrations (node_id, node_type, node_version, capabilities, endpoints, metadata, registered_at, updated_at)
105
109
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
106
110
  ON CONFLICT (node_id) DO UPDATE SET
107
111
  node_type = EXCLUDED.node_type,
@@ -114,7 +118,7 @@ RETURNING (xmax = 0) AS was_insert;
114
118
  """
115
119
 
116
120
  SQL_QUERY_BASE = """
117
- SELECT node_id, node_type, node_version, capabilities, endpoints, metadata, created_at, updated_at
121
+ SELECT node_id, node_type, node_version, capabilities, endpoints, metadata, registered_at AS created_at, updated_at
118
122
  FROM node_registrations
119
123
  """
120
124
 
@@ -384,9 +388,9 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
384
388
  result = await asyncio.wait_for(
385
389
  conn.fetchrow(
386
390
  SQL_UPSERT,
387
- record.node_id,
391
+ str(record.node_id), # VARCHAR column requires string
388
392
  record.node_type.value,
389
- record.node_version,
393
+ str(record.node_version), # VARCHAR column requires string
390
394
  capabilities_json,
391
395
  endpoints_json,
392
396
  metadata_json,
@@ -502,7 +506,7 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
502
506
  # Filter by node_id if specified (exact match)
503
507
  if query.node_id is not None:
504
508
  conditions.append(f"node_id = ${param_idx}")
505
- params.append(query.node_id)
509
+ params.append(str(query.node_id)) # VARCHAR column requires string
506
510
  param_idx += 1
507
511
 
508
512
  # Filter by node_type if specified
@@ -531,15 +535,15 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
531
535
  count_params = params[:-2] # Exclude limit and offset
532
536
 
533
537
  async with pool.acquire() as conn:
534
- rows, count_result = await asyncio.gather(
535
- asyncio.wait_for(
536
- conn.fetch(sql_query, *params),
537
- timeout=self._timeout_seconds,
538
- ),
539
- asyncio.wait_for(
540
- conn.fetchval(count_query, *count_params),
541
- timeout=self._timeout_seconds,
542
- ),
538
+ # NOTE: asyncpg connections don't support concurrent operations,
539
+ # so we run these queries sequentially instead of with asyncio.gather
540
+ rows = await asyncio.wait_for(
541
+ conn.fetch(sql_query, *params),
542
+ timeout=self._timeout_seconds,
543
+ )
544
+ count_result = await asyncio.wait_for(
545
+ conn.fetchval(count_query, *count_params),
546
+ timeout=self._timeout_seconds,
543
547
  )
544
548
 
545
549
  # Reset circuit breaker on success
@@ -555,11 +559,14 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
555
559
  endpoints = json.loads(row["endpoints"]) if row["endpoints"] else {}
556
560
  metadata = json.loads(row["metadata"]) if row["metadata"] else {}
557
561
 
562
+ # Convert database types to model types:
563
+ # - node_id: VARCHAR -> UUID
564
+ # - node_version: VARCHAR -> ModelSemVer
558
565
  records.append(
559
566
  ModelRegistrationRecord(
560
- node_id=row["node_id"],
567
+ node_id=UUID(row["node_id"]),
561
568
  node_type=EnumNodeKind(row["node_type"]),
562
- node_version=row["node_version"],
569
+ node_version=ModelSemVer.parse(row["node_version"]),
563
570
  capabilities=capabilities,
564
571
  endpoints=endpoints,
565
572
  metadata=metadata,
@@ -684,11 +691,11 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
684
691
  result = await asyncio.wait_for(
685
692
  conn.fetchval(
686
693
  SQL_UPDATE,
687
- node_id,
694
+ str(node_id), # VARCHAR column requires string
688
695
  capabilities_json,
689
696
  endpoints_json,
690
697
  metadata_json,
691
- node_version,
698
+ str(node_version) if node_version is not None else None,
692
699
  ),
693
700
  timeout=self._timeout_seconds,
694
701
  )
@@ -792,7 +799,9 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
792
799
 
793
800
  async with pool.acquire() as conn:
794
801
  result = await asyncio.wait_for(
795
- conn.fetchval(SQL_DELETE, node_id),
802
+ conn.fetchval(
803
+ SQL_DELETE, str(node_id)
804
+ ), # VARCHAR column requires string
796
805
  timeout=self._timeout_seconds,
797
806
  )
798
807
 
@@ -7,6 +7,7 @@ Reusable mixin classes providing:
7
7
  - Infrastructure error integration
8
8
  - Correlation ID propagation
9
9
  - Configurable behavior
10
+ - PostgreSQL error response building for effect persistence
10
11
 
11
12
  Exports (in __all__):
12
13
  Mixins:
@@ -14,8 +15,13 @@ Exports (in __all__):
14
15
  - MixinDictLikeAccessors: Dictionary-style access helpers
15
16
  - MixinEnvelopeExtraction: Event envelope extraction utilities
16
17
  - MixinNodeIntrospection: Node capability introspection
18
+ - MixinPostgresErrorResponse: PostgreSQL exception handling for persistence
19
+ - MixinPostgresOpExecutor: PostgreSQL operation execution with error handling
17
20
  - MixinRetryExecution: Retry logic with exponential backoff
18
21
 
22
+ Dataclasses:
23
+ - PostgresErrorContext: Context for PostgreSQL error handling
24
+
19
25
  Protocols (co-located with their tightly-coupled mixins):
20
26
  - ProtocolCircuitBreakerAware: Interface for circuit breaker capability.
21
27
  Co-located here because it is tightly coupled to MixinAsyncCircuitBreaker.
@@ -48,6 +54,11 @@ from omnibase_infra.mixins.mixin_async_circuit_breaker import MixinAsyncCircuitB
48
54
  from omnibase_infra.mixins.mixin_dict_like_accessors import MixinDictLikeAccessors
49
55
  from omnibase_infra.mixins.mixin_envelope_extraction import MixinEnvelopeExtraction
50
56
  from omnibase_infra.mixins.mixin_node_introspection import MixinNodeIntrospection
57
+ from omnibase_infra.mixins.mixin_postgres_error_response import (
58
+ MixinPostgresErrorResponse,
59
+ PostgresErrorContext,
60
+ )
61
+ from omnibase_infra.mixins.mixin_postgres_op_executor import MixinPostgresOpExecutor
51
62
  from omnibase_infra.mixins.mixin_retry_execution import MixinRetryExecution
52
63
  from omnibase_infra.mixins.protocol_circuit_breaker_aware import (
53
64
  ProtocolCircuitBreakerAware,
@@ -63,6 +74,9 @@ __all__: list[str] = [
63
74
  "MixinDictLikeAccessors",
64
75
  "MixinEnvelopeExtraction",
65
76
  "MixinNodeIntrospection",
77
+ "MixinPostgresErrorResponse",
78
+ "MixinPostgresOpExecutor",
79
+ "PostgresErrorContext",
66
80
  "MixinRetryExecution",
67
81
  "ModelCircuitBreakerConfig",
68
82
  "ModelRetryErrorClassification",
@@ -154,6 +154,7 @@ Usage:
154
154
  config = ModelIntrospectionConfig(
155
155
  node_id=node_config.node_id,
156
156
  node_type=EnumNodeKind.EFFECT,
157
+ node_name="my_node",
157
158
  event_bus=event_bus,
158
159
  )
159
160
  self.initialize_introspection(config)
@@ -214,6 +215,7 @@ from omnibase_infra.capabilities import ContractCapabilityExtractor
214
215
  from omnibase_infra.constants_topic_patterns import TOPIC_NAME_PATTERN
215
216
  from omnibase_infra.enums import EnumInfraTransportType, EnumIntrospectionReason
216
217
  from omnibase_infra.errors import ModelInfraErrorContext, ProtocolConfigurationError
218
+ from omnibase_infra.models import ModelNodeIdentity
217
219
  from omnibase_infra.models.discovery import (
218
220
  ModelDiscoveredCapabilities,
219
221
  ModelIntrospectionConfig,
@@ -422,6 +424,7 @@ class MixinNodeIntrospection:
422
424
  config = ModelIntrospectionConfig(
423
425
  node_id=node_id,
424
426
  node_type=EnumNodeKind.EFFECT,
427
+ node_name="postgres_adapter",
425
428
  event_bus=adapter_config.event_bus,
426
429
  )
427
430
  self.initialize_introspection(config)
@@ -463,6 +466,9 @@ class MixinNodeIntrospection:
463
466
  _introspection_node_type: EnumNodeKind | None
464
467
  _introspection_event_bus: ProtocolEventBus | None
465
468
  _introspection_version: str
469
+ _introspection_node_name: str
470
+ _introspection_env: str
471
+ _introspection_service: str
466
472
  _introspection_start_time: float | None
467
473
  _introspection_contract: ModelContractBase | None
468
474
 
@@ -590,6 +596,7 @@ class MixinNodeIntrospection:
590
596
  config = ModelIntrospectionConfig(
591
597
  node_id=node_config.node_id,
592
598
  node_type=EnumNodeKind.EFFECT,
599
+ node_name="my_node",
593
600
  event_bus=node_config.event_bus,
594
601
  version="1.2.0",
595
602
  )
@@ -601,6 +608,7 @@ class MixinNodeIntrospection:
601
608
  config = ModelIntrospectionConfig(
602
609
  node_id=node_config.node_id,
603
610
  node_type=EnumNodeKind.EFFECT,
611
+ node_name="my_effect_node",
604
612
  event_bus=node_config.event_bus,
605
613
  operation_keywords=frozenset({"fetch", "upload", "download"}),
606
614
  )
@@ -638,6 +646,9 @@ class MixinNodeIntrospection:
638
646
  )
639
647
  self._introspection_event_bus = config.event_bus
640
648
  self._introspection_version = config.version
649
+ self._introspection_node_name = config.node_name
650
+ self._introspection_env = config.env
651
+ self._introspection_service = config.service
641
652
  self._introspection_cache_ttl = config.cache_ttl
642
653
 
643
654
  # Capability discovery configuration - frozensets are immutable, no copy needed
@@ -984,38 +995,40 @@ class MixinNodeIntrospection:
984
995
  ) -> ModelNodeEventBusConfig | None:
985
996
  """Extract and resolve event_bus config from contract.
986
997
 
987
- Extracts topic suffixes from the contract's event_bus subcontract and
988
- resolves them to full environment-qualified topic strings.
998
+ Extracts topic names from the contract's event_bus subcontract.
999
+ Topics are realm-agnostic in ONEX - environment/realm is enforced via
1000
+ envelope identity, not topic naming.
989
1001
 
990
- Topic Resolution:
991
- Contract topics are suffixes (e.g., "onex.evt.intent-classified.v1").
992
- This method prepends the environment prefix to create full topics
993
- (e.g., "dev.onex.evt.intent-classified.v1").
1002
+ Topic Format:
1003
+ Topics follow ONEX naming convention:
1004
+ onex.{kind}.{producer}.{event-name}.v{n}
1005
+
1006
+ Example: "onex.evt.intent-classified.v1"
994
1007
 
995
1008
  Args:
996
- env_prefix: Environment prefix (e.g., "dev", "prod", "staging").
997
- Must be a valid identifier without dots or special characters.
1009
+ env_prefix: Environment prefix (retained for compatibility, but no longer
1010
+ used for topic resolution as topics are realm-agnostic).
998
1011
 
999
1012
  Returns:
1000
- Resolved event bus config with full topic strings, or None if:
1013
+ Resolved event bus config with topic strings, or None if:
1001
1014
  - No contract is configured (_introspection_contract is None)
1002
1015
  - Contract has no event_bus subcontract
1003
1016
  - event_bus subcontract has no publish_topics or subscribe_topics
1004
1017
 
1005
1018
  Raises:
1006
- ValueError: If topic resolution fails due to unresolved placeholders
1007
- (e.g., "{env}" or "{namespace}" remaining in the resolved topic).
1019
+ ValueError: If topic validation fails due to unresolved placeholders
1020
+ (e.g., "{env}" or "{namespace}" remaining in the topic).
1008
1021
  This is a fail-fast mechanism to prevent misconfigured topics
1009
1022
  from being published to the registry.
1010
1023
 
1011
1024
  Example:
1012
1025
  >>> config = self._extract_event_bus_config("dev")
1013
1026
  >>> config.publish_topic_strings
1014
- ['dev.onex.evt.node-registered.v1']
1027
+ ['onex.evt.node-registered.v1']
1015
1028
 
1016
1029
  See Also:
1017
- - ModelEventBusSubcontract: Contract model with topic suffixes
1018
- - ModelNodeEventBusConfig: Registry storage model with full topics
1030
+ - ModelEventBusSubcontract: Contract model with topics
1031
+ - ModelNodeEventBusConfig: Registry storage model
1019
1032
  """
1020
1033
  if self._introspection_contract is None:
1021
1034
  return None
@@ -1037,8 +1050,12 @@ class MixinNodeIntrospection:
1037
1050
  return None
1038
1051
 
1039
1052
  def resolve_topic(suffix: str) -> str:
1040
- """Resolve topic suffix to full topic with env prefix."""
1041
- # Full topic format: {env}.{suffix}
1053
+ """Resolve topic suffix to topic name (realm-agnostic).
1054
+
1055
+ Topics are realm-agnostic in ONEX. The environment/realm is enforced via
1056
+ envelope identity, not topic naming. This enables cross-environment event
1057
+ routing when needed while maintaining proper isolation through identity.
1058
+ """
1042
1059
  # Strip whitespace from suffix to handle YAML formatting artifacts
1043
1060
  suffix = suffix.strip()
1044
1061
 
@@ -1065,9 +1082,8 @@ class MixinNodeIntrospection:
1065
1082
  parameter="topic_suffix",
1066
1083
  )
1067
1084
 
1068
- full_topic = f"{env_prefix}.{suffix}"
1069
-
1070
- return full_topic
1085
+ # Return topic unchanged - topics are realm-agnostic
1086
+ return suffix
1071
1087
 
1072
1088
  def build_entry(suffix: str) -> ModelEventBusTopicEntry:
1073
1089
  """Build topic entry from suffix."""
@@ -2081,9 +2097,15 @@ class MixinNodeIntrospection:
2081
2097
  return False
2082
2098
 
2083
2099
  request_topic = self._request_introspection_topic
2100
+ node_identity = ModelNodeIdentity(
2101
+ env=self._introspection_env,
2102
+ service=self._introspection_service,
2103
+ node_name=self._introspection_node_name,
2104
+ version=self._introspection_version,
2105
+ )
2084
2106
  unsubscribe = await event_bus.subscribe(
2085
2107
  topic=request_topic,
2086
- group_id=f"introspection-{self._introspection_node_id}",
2108
+ node_identity=node_identity,
2087
2109
  on_message=self._handle_introspection_request,
2088
2110
  )
2089
2111
  self._registry_unsubscribe = unsubscribe
@@ -0,0 +1,314 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """PostgreSQL Error Response Mixin.
4
+
5
+ Provides standardized PostgreSQL exception handling for persistence operations
6
+ in NodeContractPersistenceEffect. Extracts the common ~60-line exception
7
+ handling pattern into a reusable mixin.
8
+
9
+ Architecture:
10
+ MixinPostgresErrorResponse is designed to be mixed into PostgreSQL
11
+ persistence classes to provide consistent error handling, sanitization,
12
+ logging, and ModelBackendResult construction.
13
+
14
+ The mixin handles:
15
+ - TimeoutError/InfraTimeoutError -> TIMEOUT_ERROR code
16
+ - InfraAuthenticationError -> AUTH_ERROR code
17
+ - InfraConnectionError -> CONNECTION_ERROR code
18
+ - RepositoryExecutionError -> operation-specific error code
19
+ - Generic Exception -> UNKNOWN_ERROR code
20
+
21
+ Error Sanitization:
22
+ All error messages are sanitized using utility functions to prevent
23
+ exposure of sensitive information (credentials, connection strings)
24
+ in logs and responses.
25
+
26
+ Logging:
27
+ - Timeout/Connection errors: logger.warning (retriable)
28
+ - Auth errors: logger.exception (non-retriable, needs attention)
29
+ - Repository errors: logger.warning (may be retriable)
30
+ - Unknown errors: logger.exception (needs investigation)
31
+
32
+ Usage:
33
+ >>> class MyPersistence(MixinPostgresErrorResponse):
34
+ ... async def handle(self, payload, correlation_id):
35
+ ... start_time = time.perf_counter()
36
+ ... try:
37
+ ... # ... database operation ...
38
+ ... except Exception as e:
39
+ ... ctx = PostgresErrorContext(
40
+ ... exception=e,
41
+ ... operation="my_operation",
42
+ ... correlation_id=correlation_id,
43
+ ... start_time=start_time,
44
+ ... log_context={"my_field": "value"},
45
+ ... operation_error_code=EnumPostgresErrorCode.UPSERT_ERROR,
46
+ ... )
47
+ ... return self._build_error_response(ctx)
48
+
49
+ Related:
50
+ - NodeContractPersistenceEffect: Parent effect node
51
+ - EnumPostgresErrorCode: Error code enumeration
52
+ - ModelBackendResult: Structured result model
53
+ - OMN-1845: Implementation ticket
54
+ """
55
+
56
+ from __future__ import annotations
57
+
58
+ import logging
59
+ import time
60
+ from dataclasses import dataclass, field
61
+ from typing import TYPE_CHECKING
62
+
63
+ from omnibase_infra.enums import EnumPostgresErrorCode
64
+ from omnibase_infra.errors import (
65
+ InfraAuthenticationError,
66
+ InfraConnectionError,
67
+ InfraTimeoutError,
68
+ RepositoryExecutionError,
69
+ )
70
+ from omnibase_infra.utils import sanitize_backend_error, sanitize_error_message
71
+
72
+ if TYPE_CHECKING:
73
+ from uuid import UUID
74
+
75
+ from omnibase_infra.models.model_backend_result import (
76
+ ModelBackendResult,
77
+ )
78
+
79
+ logger = logging.getLogger(__name__)
80
+
81
+
82
+ @dataclass
83
+ class PostgresErrorContext:
84
+ """Context for PostgreSQL error handling.
85
+
86
+ Encapsulates all parameters needed for error handling to reduce
87
+ function parameter count and improve readability.
88
+
89
+ Attributes:
90
+ exception: The exception that was raised during the operation.
91
+ operation: Name of the operation for logging.
92
+ correlation_id: Request correlation ID for distributed tracing.
93
+ start_time: Result of time.perf_counter() captured before operation.
94
+ log_context: Additional context fields for log messages.
95
+ operation_error_code: Error code for RepositoryExecutionError.
96
+ """
97
+
98
+ exception: Exception
99
+ operation: str
100
+ correlation_id: UUID
101
+ start_time: float
102
+ log_context: dict[str, object] = field(default_factory=dict)
103
+ operation_error_code: EnumPostgresErrorCode | None = None
104
+
105
+
106
+ class MixinPostgresErrorResponse:
107
+ """Mixin providing standardized PostgreSQL exception handling.
108
+
109
+ Consolidates the common exception handling pattern used across all
110
+ PostgreSQL handlers in NodeContractPersistenceEffect. This ensures
111
+ consistent error classification, sanitization, logging, and result
112
+ construction.
113
+
114
+ The mixin is designed to be used with any class that needs to handle
115
+ PostgreSQL operation errors and return ModelBackendResult.
116
+
117
+ Error Handling Matrix:
118
+ | Exception Type | Error Code | Log Level | Retriable |
119
+ |--------------------------|------------------|------------|-----------|
120
+ | TimeoutError | TIMEOUT_ERROR | warning | Yes |
121
+ | InfraTimeoutError | TIMEOUT_ERROR | warning | Yes |
122
+ | InfraAuthenticationError | AUTH_ERROR | exception | No |
123
+ | InfraConnectionError | CONNECTION_ERROR | warning | Yes |
124
+ | RepositoryExecutionError | (configurable) | warning | Maybe |
125
+ | Exception (catch-all) | UNKNOWN_ERROR | exception | No |
126
+
127
+ Example:
128
+ >>> class HandlerPostgresExample(MixinPostgresErrorResponse):
129
+ ... async def handle(self, payload, correlation_id):
130
+ ... start_time = time.perf_counter()
131
+ ... try:
132
+ ... async with self._pool.acquire() as conn:
133
+ ... await conn.execute("SELECT 1")
134
+ ... duration_ms = (time.perf_counter() - start_time) * 1000
135
+ ... return ModelBackendResult(
136
+ ... success=True,
137
+ ... duration_ms=duration_ms,
138
+ ... backend_id="postgres",
139
+ ... correlation_id=correlation_id,
140
+ ... )
141
+ ... except Exception as e:
142
+ ... ctx = PostgresErrorContext(
143
+ ... exception=e,
144
+ ... operation="example_operation",
145
+ ... correlation_id=correlation_id,
146
+ ... start_time=start_time,
147
+ ... )
148
+ ... return self._build_error_response(ctx)
149
+
150
+ See Also:
151
+ - HandlerPostgresContractUpsert: Example handler using this mixin
152
+ - HandlerPostgresTopicUpdate: Example handler using this mixin
153
+ - EnumPostgresErrorCode: Error code classification
154
+ """
155
+
156
+ def _build_error_response(
157
+ self,
158
+ ctx: PostgresErrorContext,
159
+ ) -> ModelBackendResult:
160
+ """Build ModelBackendResult for PostgreSQL operation exceptions.
161
+
162
+ Processes an exception raised during a PostgreSQL operation and
163
+ returns a properly constructed ModelBackendResult with:
164
+ - Appropriate error code based on exception type
165
+ - Sanitized error message (no credentials/PII)
166
+ - Operation duration in milliseconds
167
+ - Correlation ID for distributed tracing
168
+
169
+ Args:
170
+ ctx: PostgresErrorContext containing all error handling parameters.
171
+
172
+ Returns:
173
+ ModelBackendResult with:
174
+ - success: Always False (this is error handling)
175
+ - error: Sanitized error message safe for logs/responses
176
+ - error_code: EnumPostgresErrorCode based on exception type
177
+ - duration_ms: Operation duration in milliseconds
178
+ - backend_id: Always "postgres"
179
+ - correlation_id: Passed through for tracing
180
+
181
+ Note:
182
+ This method never raises exceptions. All error paths return
183
+ a properly constructed ModelBackendResult.
184
+ """
185
+ # Extract context fields for readability
186
+ exception = ctx.exception
187
+ operation = ctx.operation
188
+ correlation_id = ctx.correlation_id
189
+ start_time = ctx.start_time
190
+ log_context = ctx.log_context
191
+ operation_error_code = ctx.operation_error_code
192
+ # Local import to avoid circular import at module load time
193
+ # (mixins/__init__.py loads before nodes/__init__.py in some paths)
194
+ from omnibase_infra.models.model_backend_result import (
195
+ ModelBackendResult as BackendResult,
196
+ )
197
+
198
+ duration_ms = (time.perf_counter() - start_time) * 1000
199
+
200
+ # Build base log context
201
+ base_context: dict[str, object] = {
202
+ "correlation_id": str(correlation_id),
203
+ "duration_ms": duration_ms,
204
+ }
205
+
206
+ # Merge caller-provided context
207
+ if log_context:
208
+ base_context.update(log_context)
209
+
210
+ # Handle timeout errors - retriable infrastructure failures
211
+ if isinstance(exception, (TimeoutError, InfraTimeoutError)):
212
+ sanitized_error = sanitize_error_message(exception)
213
+ base_context["error"] = sanitized_error
214
+
215
+ logger.warning(
216
+ f"{operation} timed out",
217
+ extra=base_context,
218
+ )
219
+
220
+ return BackendResult(
221
+ success=False,
222
+ error=sanitized_error,
223
+ error_code=EnumPostgresErrorCode.TIMEOUT_ERROR,
224
+ duration_ms=duration_ms,
225
+ backend_id="postgres",
226
+ correlation_id=correlation_id,
227
+ )
228
+
229
+ # Handle authentication errors - non-retriable configuration failures
230
+ if isinstance(exception, InfraAuthenticationError):
231
+ sanitized_error = sanitize_error_message(exception)
232
+ base_context["error"] = sanitized_error
233
+
234
+ logger.exception(
235
+ f"{operation} authentication failed",
236
+ extra=base_context,
237
+ )
238
+
239
+ return BackendResult(
240
+ success=False,
241
+ error=sanitized_error,
242
+ error_code=EnumPostgresErrorCode.AUTH_ERROR,
243
+ duration_ms=duration_ms,
244
+ backend_id="postgres",
245
+ correlation_id=correlation_id,
246
+ )
247
+
248
+ # Handle connection errors - retriable infrastructure failures
249
+ if isinstance(exception, InfraConnectionError):
250
+ sanitized_error = sanitize_error_message(exception)
251
+ base_context["error"] = sanitized_error
252
+
253
+ logger.warning(
254
+ f"{operation} connection failed",
255
+ extra=base_context,
256
+ )
257
+
258
+ return BackendResult(
259
+ success=False,
260
+ error=sanitized_error,
261
+ error_code=EnumPostgresErrorCode.CONNECTION_ERROR,
262
+ duration_ms=duration_ms,
263
+ backend_id="postgres",
264
+ correlation_id=correlation_id,
265
+ )
266
+
267
+ # Handle repository execution errors - operation-specific failures
268
+ if isinstance(exception, RepositoryExecutionError):
269
+ sanitized_error = sanitize_error_message(exception)
270
+ base_context["error"] = sanitized_error
271
+
272
+ logger.warning(
273
+ f"{operation} execution failed",
274
+ extra=base_context,
275
+ )
276
+
277
+ # Use operation-specific error code if provided, else fall back to UNKNOWN
278
+ error_code = operation_error_code or EnumPostgresErrorCode.UNKNOWN_ERROR
279
+
280
+ return BackendResult(
281
+ success=False,
282
+ error=sanitized_error,
283
+ error_code=error_code,
284
+ duration_ms=duration_ms,
285
+ backend_id="postgres",
286
+ correlation_id=correlation_id,
287
+ )
288
+
289
+ # Generic catch-all for unexpected exceptions
290
+ # This catch-all is required because database adapters may raise
291
+ # unexpected exceptions beyond typed infrastructure errors (e.g.,
292
+ # driver errors, encoding errors, connection pool errors, asyncpg-
293
+ # specific exceptions). All errors must be sanitized to prevent
294
+ # credential exposure.
295
+ sanitized_error = sanitize_backend_error("postgres", exception)
296
+ base_context["error"] = sanitized_error
297
+ base_context["error_type"] = type(exception).__name__
298
+
299
+ logger.exception(
300
+ f"{operation} failed with unexpected error",
301
+ extra=base_context,
302
+ )
303
+
304
+ return BackendResult(
305
+ success=False,
306
+ error=sanitized_error,
307
+ error_code=EnumPostgresErrorCode.UNKNOWN_ERROR,
308
+ duration_ms=duration_ms,
309
+ backend_id="postgres",
310
+ correlation_id=correlation_id,
311
+ )
312
+
313
+
314
+ __all__: list[str] = ["MixinPostgresErrorResponse", "PostgresErrorContext"]