omnibase_infra 0.3.2__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 (57) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/errors/__init__.py +4 -0
  3. omnibase_infra/errors/error_infra.py +60 -0
  4. omnibase_infra/handlers/__init__.py +3 -0
  5. omnibase_infra/handlers/handler_slack_webhook.py +426 -0
  6. omnibase_infra/handlers/models/__init__.py +14 -0
  7. omnibase_infra/handlers/models/enum_alert_severity.py +36 -0
  8. omnibase_infra/handlers/models/model_slack_alert.py +24 -0
  9. omnibase_infra/handlers/models/model_slack_alert_payload.py +77 -0
  10. omnibase_infra/handlers/models/model_slack_alert_result.py +73 -0
  11. omnibase_infra/mixins/mixin_node_introspection.py +42 -20
  12. omnibase_infra/models/discovery/model_dependency_spec.py +1 -0
  13. omnibase_infra/models/discovery/model_discovered_capabilities.py +1 -1
  14. omnibase_infra/models/discovery/model_introspection_config.py +28 -1
  15. omnibase_infra/models/discovery/model_introspection_performance_metrics.py +1 -0
  16. omnibase_infra/models/discovery/model_introspection_task_config.py +1 -0
  17. omnibase_infra/models/runtime/__init__.py +4 -0
  18. omnibase_infra/models/runtime/model_resolved_dependencies.py +116 -0
  19. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +6 -5
  20. omnibase_infra/nodes/contract_registry_reducer/reducer.py +9 -26
  21. omnibase_infra/nodes/node_contract_persistence_effect/node.py +18 -1
  22. omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +33 -2
  23. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +8 -12
  24. omnibase_infra/nodes/node_slack_alerter_effect/__init__.py +33 -0
  25. omnibase_infra/nodes/node_slack_alerter_effect/contract.yaml +291 -0
  26. omnibase_infra/nodes/node_slack_alerter_effect/node.py +106 -0
  27. omnibase_infra/runtime/__init__.py +7 -0
  28. omnibase_infra/runtime/baseline_subscriptions.py +13 -6
  29. omnibase_infra/runtime/contract_dependency_resolver.py +455 -0
  30. omnibase_infra/runtime/contract_registration_event_router.py +5 -5
  31. omnibase_infra/runtime/emit_daemon/event_registry.py +34 -22
  32. omnibase_infra/runtime/event_bus_subcontract_wiring.py +63 -23
  33. omnibase_infra/runtime/publisher_topic_scoped.py +16 -11
  34. omnibase_infra/runtime/registry_policy.py +29 -15
  35. omnibase_infra/runtime/request_response_wiring.py +15 -7
  36. omnibase_infra/runtime/service_runtime_host_process.py +149 -5
  37. omnibase_infra/runtime/util_version.py +5 -1
  38. omnibase_infra/schemas/schema_latency_baseline.sql +135 -0
  39. omnibase_infra/services/contract_publisher/config.py +4 -4
  40. omnibase_infra/services/contract_publisher/service.py +8 -5
  41. omnibase_infra/services/observability/injection_effectiveness/__init__.py +67 -0
  42. omnibase_infra/services/observability/injection_effectiveness/config.py +295 -0
  43. omnibase_infra/services/observability/injection_effectiveness/consumer.py +1461 -0
  44. omnibase_infra/services/observability/injection_effectiveness/models/__init__.py +32 -0
  45. omnibase_infra/services/observability/injection_effectiveness/models/model_agent_match.py +79 -0
  46. omnibase_infra/services/observability/injection_effectiveness/models/model_context_utilization.py +118 -0
  47. omnibase_infra/services/observability/injection_effectiveness/models/model_latency_breakdown.py +107 -0
  48. omnibase_infra/services/observability/injection_effectiveness/models/model_pattern_utilization.py +46 -0
  49. omnibase_infra/services/observability/injection_effectiveness/writer_postgres.py +596 -0
  50. omnibase_infra/utils/__init__.py +7 -0
  51. omnibase_infra/utils/util_db_error_context.py +292 -0
  52. omnibase_infra/validation/validation_exemptions.yaml +11 -0
  53. {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/METADATA +2 -2
  54. {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/RECORD +57 -36
  55. {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/WHEEL +0 -0
  56. {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/entry_points.txt +0 -0
  57. {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,455 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Contract dependency resolver for ONEX nodes.
4
+
5
+ This module provides ContractDependencyResolver, which reads protocol
6
+ dependencies from a node's contract.yaml and resolves them from the
7
+ container's service_registry.
8
+
9
+ Part of OMN-1732: Runtime dependency injection for zero-code nodes.
10
+
11
+ Architecture:
12
+ - Container: service provider (owns protocol instances)
13
+ - Runtime (this resolver): wiring authority (resolves + validates)
14
+ - Node: consumer (receives resolved deps via constructor)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import importlib
20
+ import logging
21
+ from pathlib import Path
22
+ from types import SimpleNamespace
23
+ from typing import TYPE_CHECKING, Any, cast
24
+
25
+ import yaml
26
+
27
+ from omnibase_infra.enums import EnumInfraTransportType
28
+ from omnibase_infra.errors import (
29
+ ModelInfraErrorContext,
30
+ ProtocolConfigurationError,
31
+ ProtocolDependencyResolutionError,
32
+ )
33
+ from omnibase_infra.models.runtime.model_resolved_dependencies import (
34
+ ModelResolvedDependencies,
35
+ )
36
+
37
+ if TYPE_CHECKING:
38
+ from omnibase_core.container import ModelONEXContainer
39
+ from omnibase_core.models.contracts import ModelContractBase
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ class ContractDependencyResolver:
45
+ """Resolves protocol dependencies from contracts using container.
46
+
47
+ Reads the `dependencies` field from a node's contract, filters for
48
+ protocol-type dependencies, imports the protocol classes, and resolves
49
+ instances from the container's service_registry.
50
+
51
+ This resolver implements the "fail-fast" principle: if any required
52
+ protocol cannot be resolved, it raises ProtocolDependencyResolutionError
53
+ immediately rather than allowing node creation with missing dependencies.
54
+
55
+ Example:
56
+ >>> from omnibase_core.container import ModelONEXContainer
57
+ >>>
58
+ >>> container = ModelONEXContainer()
59
+ >>> # ... register protocols in container ...
60
+ >>>
61
+ >>> resolver = ContractDependencyResolver(container)
62
+ >>> resolved = await resolver.resolve(node_contract)
63
+ >>>
64
+ >>> # Use resolved dependencies for node creation
65
+ >>> node = NodeRegistry.create(container, dependencies=resolved)
66
+
67
+ .. versionadded:: 0.x.x
68
+ Part of OMN-1732 runtime dependency injection.
69
+ """
70
+
71
+ def __init__(self, container: ModelONEXContainer) -> None:
72
+ """Initialize the resolver with a container.
73
+
74
+ Args:
75
+ container: ONEX container with service_registry for protocol resolution.
76
+ """
77
+ self._container = container
78
+
79
+ async def resolve(
80
+ self,
81
+ contract: ModelContractBase,
82
+ *,
83
+ allow_missing: bool = False,
84
+ ) -> ModelResolvedDependencies:
85
+ """Resolve protocol dependencies from a contract.
86
+
87
+ Reads contract.dependencies, filters for protocol-type entries,
88
+ and resolves each from the container's service_registry.
89
+
90
+ Args:
91
+ contract: The node contract containing dependency declarations.
92
+ allow_missing: If True, skip missing protocols instead of raising.
93
+ Default False (fail-fast behavior).
94
+
95
+ Returns:
96
+ ModelResolvedDependencies containing resolved protocol instances.
97
+
98
+ Raises:
99
+ ProtocolDependencyResolutionError: If any required protocol cannot
100
+ be resolved and allow_missing is False.
101
+
102
+ Example:
103
+ >>> resolved = await resolver.resolve(contract)
104
+ >>> adapter = resolved.get("ProtocolPostgresAdapter")
105
+ """
106
+ # Extract protocol dependencies from contract
107
+ protocol_deps = self._extract_protocol_dependencies(contract)
108
+
109
+ if not protocol_deps:
110
+ logger.debug(
111
+ "No protocol dependencies declared in contract",
112
+ extra={"contract_name": getattr(contract, "name", "unknown")},
113
+ )
114
+ return ModelResolvedDependencies()
115
+
116
+ # Resolve each protocol from container
117
+ # ONEX_EXCLUDE: any_type - dict holds heterogeneous protocol instances resolved at runtime
118
+ resolved: dict[str, Any] = {}
119
+ missing: list[str] = []
120
+ resolution_errors: dict[str, str] = {}
121
+
122
+ for dep in protocol_deps:
123
+ class_name = dep.get("class_name")
124
+ module_path = dep.get("module")
125
+
126
+ if not class_name:
127
+ logger.warning(
128
+ "Protocol dependency missing class_name, skipping",
129
+ extra={"dependency": dep},
130
+ )
131
+ continue
132
+
133
+ try:
134
+ # Import the protocol class
135
+ protocol_class = self._import_protocol_class(class_name, module_path)
136
+
137
+ # Resolve from container
138
+ instance = await self._resolve_from_container(protocol_class)
139
+ resolved[class_name] = instance
140
+
141
+ logger.debug(
142
+ "Resolved protocol dependency",
143
+ extra={
144
+ "protocol": class_name,
145
+ "module_path": module_path,
146
+ },
147
+ )
148
+
149
+ except Exception as e:
150
+ error_msg = str(e)
151
+ resolution_errors[class_name] = error_msg
152
+ missing.append(class_name)
153
+
154
+ logger.warning(
155
+ "Failed to resolve protocol dependency",
156
+ extra={
157
+ "protocol": class_name,
158
+ "module_path": module_path,
159
+ "error": error_msg,
160
+ },
161
+ )
162
+
163
+ # Fail-fast if any protocols are missing
164
+ if missing and not allow_missing:
165
+ contract_name = getattr(contract, "name", "unknown")
166
+ context = ModelInfraErrorContext.with_correlation(
167
+ transport_type=EnumInfraTransportType.RUNTIME,
168
+ operation="resolve_dependencies",
169
+ target_name=contract_name,
170
+ )
171
+
172
+ error_details = "\n".join(
173
+ f" - {proto}: {resolution_errors.get(proto, 'unknown error')}"
174
+ for proto in missing
175
+ )
176
+
177
+ raise ProtocolDependencyResolutionError(
178
+ f"Cannot create node '{contract_name}': missing required protocols.\n"
179
+ f"The following protocols are declared in contract.yaml but could not "
180
+ f"be resolved from the container:\n{error_details}\n\n"
181
+ f"Ensure these protocols are registered in the container before "
182
+ f"node creation via container.service_registry.register_instance().",
183
+ context=context,
184
+ missing_protocols=missing,
185
+ node_name=contract_name,
186
+ )
187
+
188
+ return ModelResolvedDependencies(protocols=resolved)
189
+
190
+ def _extract_protocol_dependencies(
191
+ self,
192
+ contract: ModelContractBase,
193
+ ) -> list[dict[str, str]]:
194
+ """Extract protocol-type dependencies from contract.
195
+
196
+ Supports multiple detection patterns:
197
+ 1. `type` field = "protocol" (YAML contract style)
198
+ 2. `is_protocol()` method returns True
199
+ 3. `dependency_type` attribute with value "PROTOCOL"
200
+
201
+ Args:
202
+ contract: The contract to extract dependencies from.
203
+
204
+ Returns:
205
+ List of dependency dicts with keys: name, type, class_name, module
206
+ """
207
+ dependencies: list[dict[str, str]] = []
208
+
209
+ # Get dependencies attribute if present
210
+ contract_deps = getattr(contract, "dependencies", None)
211
+ if not contract_deps:
212
+ return dependencies
213
+
214
+ for dep in contract_deps:
215
+ is_protocol = self._is_protocol_dependency(dep)
216
+
217
+ if is_protocol:
218
+ dep_dict: dict[str, str] = {}
219
+
220
+ # Extract fields - handle both attribute and dict access
221
+ if hasattr(dep, "name"):
222
+ dep_dict["name"] = str(dep.name) if dep.name else ""
223
+ if hasattr(dep, "class_name"):
224
+ dep_dict["class_name"] = (
225
+ str(dep.class_name) if dep.class_name else ""
226
+ )
227
+ if hasattr(dep, "module"):
228
+ dep_dict["module"] = str(dep.module) if dep.module else ""
229
+
230
+ # Only add if we have a class_name
231
+ if dep_dict.get("class_name"):
232
+ dependencies.append(dep_dict)
233
+
234
+ return dependencies
235
+
236
+ # ONEX_EXCLUDE: any_type - dep can be ModelContractDependency or dict from various sources
237
+ def _is_protocol_dependency(self, dep: Any) -> bool:
238
+ """Check if a dependency is a protocol dependency.
239
+
240
+ Supports multiple detection patterns:
241
+ 1. `type` field = "protocol" (YAML contract style)
242
+ 2. `is_protocol()` method returns True
243
+ 3. `dependency_type` attribute with value "PROTOCOL"
244
+
245
+ Args:
246
+ dep: The dependency object to check.
247
+
248
+ Returns:
249
+ True if this is a protocol dependency, False otherwise.
250
+ """
251
+ # Check is_protocol() method first (most explicit)
252
+ if hasattr(dep, "is_protocol") and callable(dep.is_protocol):
253
+ if dep.is_protocol():
254
+ return True
255
+
256
+ # Check dependency_type attribute (enum or string)
257
+ if hasattr(dep, "dependency_type"):
258
+ dep_type_val = dep.dependency_type
259
+ if hasattr(dep_type_val, "value"):
260
+ # Enum with .value
261
+ if str(dep_type_val.value).upper() == "PROTOCOL":
262
+ return True
263
+ elif str(dep_type_val).upper() == "PROTOCOL":
264
+ return True
265
+
266
+ # Check type field (YAML contract style: type: "protocol")
267
+ if hasattr(dep, "type"):
268
+ type_val = getattr(dep, "type", "")
269
+ if str(type_val).lower() == "protocol":
270
+ return True
271
+
272
+ return False
273
+
274
+ def _import_protocol_class(
275
+ self,
276
+ class_name: str,
277
+ module_path: str | None,
278
+ ) -> type:
279
+ """Import a protocol class by name and module.
280
+
281
+ Args:
282
+ class_name: The class name to import
283
+ module_path: The module path (required for import)
284
+
285
+ Returns:
286
+ The imported class type.
287
+
288
+ Raises:
289
+ ImportError: If the class cannot be imported.
290
+ """
291
+ if not module_path:
292
+ raise ImportError(
293
+ f"Protocol '{class_name}' has no module path specified in contract. "
294
+ f"Cannot import without module path."
295
+ )
296
+
297
+ try:
298
+ module = importlib.import_module(module_path)
299
+ protocol_class = getattr(module, class_name)
300
+ return cast("type", protocol_class)
301
+ except ModuleNotFoundError as e:
302
+ raise ImportError(
303
+ f"Module '{module_path}' not found for protocol '{class_name}': {e}"
304
+ ) from e
305
+ except AttributeError as e:
306
+ raise ImportError(
307
+ f"Class '{class_name}' not found in module '{module_path}': {e}"
308
+ ) from e
309
+
310
+ # ONEX_EXCLUDE: any_type - returns protocol instance, type varies by resolved protocol class
311
+ async def _resolve_from_container(self, protocol_class: type) -> Any:
312
+ """Resolve a protocol instance from the container.
313
+
314
+ Args:
315
+ protocol_class: The protocol class type to resolve.
316
+
317
+ Returns:
318
+ The resolved protocol instance.
319
+
320
+ Raises:
321
+ RuntimeError: If container.service_registry is None.
322
+ Exception: If resolution fails (propagated from container).
323
+ """
324
+ if self._container.service_registry is None:
325
+ raise RuntimeError(
326
+ "Container service_registry is None. "
327
+ "Ensure container is properly initialized with service_registry enabled."
328
+ )
329
+
330
+ return await self._container.service_registry.resolve_service(protocol_class)
331
+
332
+ async def resolve_from_path(
333
+ self,
334
+ contract_path: Path,
335
+ *,
336
+ allow_missing: bool = False,
337
+ ) -> ModelResolvedDependencies:
338
+ """Resolve protocol dependencies from a contract.yaml file path.
339
+
340
+ Loads the contract YAML, extracts the dependencies section, and resolves
341
+ each protocol from the container's service_registry. This is a convenience
342
+ method that combines file loading with dependency resolution.
343
+
344
+ Part of OMN-1903: Runtime dependency injection integration.
345
+
346
+ Args:
347
+ contract_path: Path object to the contract.yaml file.
348
+ allow_missing: If True, skip missing protocols instead of raising.
349
+ Default False (fail-fast behavior).
350
+
351
+ Returns:
352
+ ModelResolvedDependencies containing resolved protocol instances.
353
+ Returns empty ModelResolvedDependencies if contract has no dependencies.
354
+
355
+ Raises:
356
+ ProtocolConfigurationError: If contract file cannot be loaded or parsed.
357
+ ProtocolDependencyResolutionError: If any required protocol cannot
358
+ be resolved and allow_missing is False.
359
+
360
+ Example:
361
+ >>> resolver = ContractDependencyResolver(container)
362
+ >>> resolved = await resolver.resolve_from_path(
363
+ ... Path("src/nodes/my_node/contract.yaml")
364
+ ... )
365
+ >>> adapter = resolved.get("ProtocolPostgresAdapter")
366
+
367
+ .. versionadded:: 0.x.x
368
+ Part of OMN-1903 runtime dependency injection integration.
369
+ """
370
+ path = contract_path
371
+
372
+ # Load and parse the contract YAML
373
+ contract_data = self._load_contract_yaml(path)
374
+
375
+ # Check if contract has dependencies section
376
+ if "dependencies" not in contract_data or not contract_data["dependencies"]:
377
+ logger.debug(
378
+ "No dependencies section in contract, skipping resolution",
379
+ extra={"contract_path": str(path)},
380
+ )
381
+ return ModelResolvedDependencies()
382
+
383
+ # Create a lightweight contract object for the resolver
384
+ # Uses SimpleNamespace for duck-typing compatibility with resolve()
385
+ contract_name = contract_data.get("name", path.stem)
386
+ dependencies = [SimpleNamespace(**dep) for dep in contract_data["dependencies"]]
387
+ contract = SimpleNamespace(name=contract_name, dependencies=dependencies)
388
+
389
+ logger.debug(
390
+ "Resolving dependencies from contract path",
391
+ extra={
392
+ "contract_path": str(path),
393
+ "contract_name": contract_name,
394
+ "dependency_count": len(dependencies),
395
+ },
396
+ )
397
+
398
+ # Type ignore: contract is a SimpleNamespace duck-typed to match ModelContractBase
399
+ # The resolver uses getattr() internally so any object with name/dependencies works
400
+ return await self.resolve(contract, allow_missing=allow_missing) # type: ignore[arg-type]
401
+
402
+ # ONEX_EXCLUDE: any_type - yaml.safe_load returns heterogeneous dict, values vary by contract schema
403
+ def _load_contract_yaml(self, path: Path) -> dict[str, Any]:
404
+ """Load and parse a contract.yaml file.
405
+
406
+ Args:
407
+ path: Path to the contract YAML file.
408
+
409
+ Returns:
410
+ Parsed YAML content as a dictionary.
411
+
412
+ Raises:
413
+ ProtocolConfigurationError: If file doesn't exist or cannot be parsed.
414
+ """
415
+ if not path.exists():
416
+ context = ModelInfraErrorContext.with_correlation(
417
+ transport_type=EnumInfraTransportType.FILESYSTEM,
418
+ operation="load_contract_yaml",
419
+ target_name=str(path),
420
+ )
421
+ raise ProtocolConfigurationError(
422
+ f"Contract file not found: {path}",
423
+ context=context,
424
+ )
425
+
426
+ try:
427
+ with open(path, encoding="utf-8") as f:
428
+ data = yaml.safe_load(f)
429
+ if data is None:
430
+ data = {}
431
+ # ONEX_EXCLUDE: any_type - yaml.safe_load returns Any, we validate structure elsewhere
432
+ return cast("dict[str, Any]", data)
433
+ except yaml.YAMLError as e:
434
+ context = ModelInfraErrorContext.with_correlation(
435
+ transport_type=EnumInfraTransportType.FILESYSTEM,
436
+ operation="load_contract_yaml",
437
+ target_name=str(path),
438
+ )
439
+ raise ProtocolConfigurationError(
440
+ f"Failed to parse contract YAML at {path}: {e}",
441
+ context=context,
442
+ ) from e
443
+ except OSError as e:
444
+ context = ModelInfraErrorContext.with_correlation(
445
+ transport_type=EnumInfraTransportType.FILESYSTEM,
446
+ operation="load_contract_yaml",
447
+ target_name=str(path),
448
+ )
449
+ raise ProtocolConfigurationError(
450
+ f"Failed to read contract file at {path}: {e}",
451
+ context=context,
452
+ ) from e
453
+
454
+
455
+ __all__ = ["ContractDependencyResolver"]
@@ -11,10 +11,10 @@ The router:
11
11
  - Routes to ContractRegistryReducer.reduce() for state transitions
12
12
  - Emits intents for Effect layer execution (PostgreSQL operations)
13
13
 
14
- Topics Handled:
15
- - {env}.onex.evt.platform.contract-registered.v1 -> ModelContractRegisteredEvent
16
- - {env}.onex.evt.platform.contract-deregistered.v1 -> ModelContractDeregisteredEvent
17
- - {env}.onex.evt.platform.node-heartbeat.v1 -> ModelNodeHeartbeatEvent
14
+ Topics Handled (realm-agnostic, no environment prefix):
15
+ - onex.evt.platform.contract-registered.v1 -> ModelContractRegisteredEvent
16
+ - onex.evt.platform.contract-deregistered.v1 -> ModelContractDeregisteredEvent
17
+ - onex.evt.platform.node-heartbeat.v1 -> ModelNodeHeartbeatEvent
18
18
 
19
19
  Design:
20
20
  This class encapsulates the message routing logic for contract registration
@@ -69,7 +69,7 @@ if TYPE_CHECKING:
69
69
  logger = logging.getLogger(__name__)
70
70
 
71
71
  # Topic suffix patterns for event type matching
72
- # These match the topic suffix after the environment prefix
72
+ # Topics are realm-agnostic (no environment prefix)
73
73
  TOPIC_SUFFIX_CONTRACT_REGISTERED = "onex.evt.platform.contract-registered.v1"
74
74
  TOPIC_SUFFIX_CONTRACT_DEREGISTERED = "onex.evt.platform.contract-deregistered.v1"
75
75
  TOPIC_SUFFIX_NODE_HEARTBEAT = "onex.evt.platform.node-heartbeat.v1"
@@ -25,15 +25,15 @@ Example Usage:
25
25
  registry.register(
26
26
  ModelEventRegistration(
27
27
  event_type="custom.event",
28
- topic_template="{env}.onex.evt.custom.event.v1",
28
+ topic_template="onex.evt.custom.event.v1",
29
29
  partition_key_field="session_id",
30
30
  required_fields=["session_id", "user_id"],
31
31
  )
32
32
  )
33
33
 
34
- # Resolve topic for event type
34
+ # Resolve topic for event type (realm-agnostic, no env prefix)
35
35
  topic = registry.resolve_topic("prompt.submitted")
36
- # Returns: "dev.onex.evt.omniclaude.prompt-submitted.v1"
36
+ # Returns: "onex.evt.omniclaude.prompt-submitted.v1"
37
37
 
38
38
  # Inject metadata into payload
39
39
  enriched = registry.inject_metadata(
@@ -70,8 +70,10 @@ class ModelEventRegistration(BaseModel):
70
70
  Attributes:
71
71
  event_type: Semantic event type identifier (e.g., "prompt.submitted").
72
72
  This is the logical name used by event emitters.
73
- topic_template: Kafka topic name template with {env} placeholder.
74
- Example: "{env}.onex.evt.omniclaude.prompt-submitted.v1"
73
+ topic_template: Kafka topic name (realm-agnostic, no environment prefix).
74
+ Example: "onex.evt.omniclaude.prompt-submitted.v1"
75
+ Note: Topics are realm-agnostic in ONEX. The environment/realm is
76
+ enforced via envelope identity, not topic naming.
75
77
  partition_key_field: Optional field name in payload to use as partition key.
76
78
  When set, ensures events with same key go to same partition for ordering.
77
79
  required_fields: List of field names that must be present in payload.
@@ -82,7 +84,7 @@ class ModelEventRegistration(BaseModel):
82
84
  Example:
83
85
  >>> reg = ModelEventRegistration(
84
86
  ... event_type="prompt.submitted",
85
- ... topic_template="{env}.onex.evt.omniclaude.prompt-submitted.v1",
87
+ ... topic_template="onex.evt.omniclaude.prompt-submitted.v1",
86
88
  ... partition_key_field="session_id",
87
89
  ... required_fields=["prompt", "session_id"],
88
90
  ... schema_version="1.0.0",
@@ -99,7 +101,7 @@ class ModelEventRegistration(BaseModel):
99
101
  description="Semantic event type identifier (e.g., 'prompt.submitted')",
100
102
  )
101
103
  topic_template: str = Field(
102
- description="Kafka topic name template with {env} placeholder",
104
+ description="Kafka topic name (realm-agnostic, no environment prefix)",
103
105
  )
104
106
  partition_key_field: str | None = Field(
105
107
  default=None,
@@ -133,7 +135,7 @@ class EventRegistry:
133
135
  >>> registry = EventRegistry(environment="dev")
134
136
  >>> topic = registry.resolve_topic("prompt.submitted")
135
137
  >>> print(topic)
136
- 'dev.onex.evt.omniclaude.prompt-submitted.v1'
138
+ 'onex.evt.omniclaude.prompt-submitted.v1'
137
139
 
138
140
  >>> registry.validate_payload("prompt.submitted", {"prompt": "Hello"})
139
141
  True
@@ -144,6 +146,11 @@ class EventRegistry:
144
146
  ... )
145
147
  >>> "correlation_id" in enriched
146
148
  True
149
+
150
+ Note:
151
+ Topics are realm-agnostic in ONEX. The environment is stored for
152
+ potential use in consumer group derivation by related components,
153
+ but topics themselves do not include environment prefixes.
147
154
  """
148
155
 
149
156
  def __init__(self, environment: str = "dev") -> None:
@@ -156,7 +163,11 @@ class EventRegistry:
156
163
  Example:
157
164
  >>> registry = EventRegistry(environment="staging")
158
165
  >>> registry.resolve_topic("prompt.submitted")
159
- 'staging.onex.evt.omniclaude.prompt-submitted.v1'
166
+ 'onex.evt.omniclaude.prompt-submitted.v1'
167
+
168
+ Note:
169
+ The environment is stored for potential consumer group derivation
170
+ in related components. Topics themselves are realm-agnostic.
160
171
  """
161
172
  self._environment = environment
162
173
  self._registrations: dict[str, ModelEventRegistration] = {}
@@ -174,25 +185,25 @@ class EventRegistry:
174
185
  defaults = [
175
186
  ModelEventRegistration(
176
187
  event_type="prompt.submitted",
177
- topic_template="{env}.onex.evt.omniclaude.prompt-submitted.v1",
188
+ topic_template="onex.evt.omniclaude.prompt-submitted.v1",
178
189
  partition_key_field="session_id",
179
190
  required_fields=["prompt"],
180
191
  ),
181
192
  ModelEventRegistration(
182
193
  event_type="session.started",
183
- topic_template="{env}.onex.evt.omniclaude.session-started.v1",
194
+ topic_template="onex.evt.omniclaude.session-started.v1",
184
195
  partition_key_field="session_id",
185
196
  required_fields=["session_id"],
186
197
  ),
187
198
  ModelEventRegistration(
188
199
  event_type="session.ended",
189
- topic_template="{env}.onex.evt.omniclaude.session-ended.v1",
200
+ topic_template="onex.evt.omniclaude.session-ended.v1",
190
201
  partition_key_field="session_id",
191
202
  required_fields=["session_id"],
192
203
  ),
193
204
  ModelEventRegistration(
194
205
  event_type="tool.executed",
195
- topic_template="{env}.onex.evt.omniclaude.tool-executed.v1",
206
+ topic_template="onex.evt.omniclaude.tool-executed.v1",
196
207
  partition_key_field="session_id",
197
208
  required_fields=["tool_name"],
198
209
  ),
@@ -214,25 +225,26 @@ class EventRegistry:
214
225
  >>> registry.register(
215
226
  ... ModelEventRegistration(
216
227
  ... event_type="custom.event",
217
- ... topic_template="{env}.onex.evt.custom.event.v1",
228
+ ... topic_template="onex.evt.custom.event.v1",
218
229
  ... )
219
230
  ... )
220
231
  >>> registry.resolve_topic("custom.event")
221
- 'dev.onex.evt.custom.event.v1'
232
+ 'onex.evt.custom.event.v1'
222
233
  """
223
234
  self._registrations[registration.event_type] = registration
224
235
 
225
236
  def resolve_topic(self, event_type: str) -> str:
226
- """Get the Kafka topic for an event type.
237
+ """Get the Kafka topic for an event type (realm-agnostic).
227
238
 
228
- Resolves the topic template by substituting the {env} placeholder
229
- with the configured environment name.
239
+ Topics are realm-agnostic in ONEX. The environment/realm is enforced via
240
+ envelope identity, not topic naming. This enables cross-environment event
241
+ routing when needed while maintaining proper isolation through identity.
230
242
 
231
243
  Args:
232
244
  event_type: Semantic event type identifier.
233
245
 
234
246
  Returns:
235
- Fully resolved Kafka topic name.
247
+ Kafka topic name (no environment prefix).
236
248
 
237
249
  Raises:
238
250
  OnexError: If the event type is not registered.
@@ -240,7 +252,7 @@ class EventRegistry:
240
252
  Example:
241
253
  >>> registry = EventRegistry(environment="prod")
242
254
  >>> registry.resolve_topic("prompt.submitted")
243
- 'prod.onex.evt.omniclaude.prompt-submitted.v1'
255
+ 'onex.evt.omniclaude.prompt-submitted.v1'
244
256
  """
245
257
  registration = self._registrations.get(event_type)
246
258
  if registration is None:
@@ -248,7 +260,7 @@ class EventRegistry:
248
260
  raise OnexError(
249
261
  f"Unknown event type: '{event_type}'. Registered types: {registered}"
250
262
  )
251
- return registration.topic_template.format(env=self._environment)
263
+ return registration.topic_template
252
264
 
253
265
  def get_partition_key(
254
266
  self,
@@ -452,7 +464,7 @@ class EventRegistry:
452
464
  >>> registry = EventRegistry()
453
465
  >>> reg = registry.get_registration("prompt.submitted")
454
466
  >>> reg.topic_template
455
- '{env}.onex.evt.omniclaude.prompt-submitted.v1'
467
+ 'onex.evt.omniclaude.prompt-submitted.v1'
456
468
  """
457
469
  return self._registrations.get(event_type)
458
470