Repository navigation
feat(gateway): Add runtime gateway with envelope signing and realm enforcement [OMN-1899] - #249
Conversation
…forcement [OMN-1899] Implement runtime gateway functionality that signs outbound envelopes and validates inbound envelopes, enforcing realm boundaries per the ONEX security architecture. Components added: - ModelGatewayConfig: Configuration for realm, runtime_id, keys, policies - ServiceEnvelopeSigner: Signs payloads with Ed25519, wraps in ModelMessageEnvelope - ServiceEnvelopeValidator: Validates signatures, realms, detects tampering - ServicePolicyEngine: Enforces topic allowlist with wildcard support - Key loader utilities for Ed25519 PEM files Runtime integration: - Gateway initialization in RuntimeHostProcess.start() - Outbound signing in _publish_envelope_safe() - Inbound validation in _handle_envelope() - Graceful degradation when gateway disabled (default) Dependencies: - Upgraded omnibase-core to 0.15.0 for ModelMessageEnvelope - Added blake3 for cryptographic hashing Validation updates: - Increased INFRA_MAX_UNIONS to 119 (EnumPolicyDecision union) - Added pattern exemptions for runtime_id (str by design) and sign_envelope params Tests: 92 integration tests covering signing, validation, policy, and config
📝 WalkthroughWalkthroughAdds a gateway subsystem (signer, validator, policy engine, key loaders, config model), exposes a consolidated gateway package API, wires lazy gateway initialization and inbound/outbound signing/validation into RuntimeHostProcess, bumps Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client/Service
participant Runtime as RuntimeHostProcess
participant Policy as ServicePolicyEngine
participant Signer as ServiceEnvelopeSigner
participant Validator as ServiceEnvelopeValidator
Client->>Runtime: publish(payload, topic)
Runtime->>Policy: evaluate_outbound(topic)
Policy-->>Runtime: PolicyDecision (allow/deny)
alt allowed and signer configured
Runtime->>Signer: sign_envelope(payload, bus_id, ...)
Signer-->>Runtime: ModelMessageEnvelope (signed)
end
Runtime->>Runtime: serialize / publish to bus
Note right of Runtime: incoming message received
Runtime->>Validator: validate_envelope(incoming_envelope)
Validator->>Validator: check realm, recompute Blake3(hash), lookup signer
Validator->>Validator: verify Ed25519 signature
Validator-->>Runtime: ValidationResult (is_valid / error_code)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/omnibase_infra/runtime/service_runtime_host_process.py (1)
55-55:⚠️ Potential issue | 🟠 MajorDict envelopes skip gateway signing and gateway logs lack correlation_id
When gateway signing is enabled, only BaseModel payloads are signed (line 2533). Error and duplicate responses are published as dicts via
_create_error_response()and_create_duplicate_response().model_dump(), bypassing the signer and causing rejection by downstream runtimes withreject_unsigned=True.Additionally, gateway validation rejection logs (lines 3778–3851) lack
correlation_idin their structured context, violating the coding guideline to include correlation_id in all error contexts for observability.Recommended fixes:
- Wrap dict envelopes in
RootModel[dict[str, object]]before signing, or extract correlation_id from dict envelopes and pass it to the signer- Extract
correlation_idfrom inbound envelope and include it in all gateway rejection log messages
🤖 Fix all issues with AI agents
In `@src/omnibase_infra/gateway/services/service_policy_engine.py`:
- Around line 223-277: Add an optional correlation_id parameter to
evaluate_inbound (and mirror the same change in evaluate_outbound) so callers
can pass a trace id; inside each function, if correlation_id is None generate
one with uuid.uuid4(); when calling _log_rejection("inbound"/"outbound", topic,
reason) include the correlation_id in the log extra/context payload so the
rejection warnings carry the id; update any return paths that call
_log_rejection to forward the generated or provided correlation_id and ensure
function signatures (evaluate_inbound, evaluate_outbound) and any call sites are
updated to accept/propagate the new parameter.
In `@src/omnibase_infra/gateway/utils/util_key_loader.py`:
- Around line 46-125: The load_private_key_from_pem function currently calls
load_pem_private_key but doesn't catch
cryptography.exceptions.UnsupportedAlgorithm; update the except block that
handles load_pem_private_key to also catch UnsupportedAlgorithm (alongside
ValueError and TypeError), log the parsing failure (using the existing
logger.exception call with context.correlation_id and path) and raise a
ProtocolConfigurationError (preserving context and chaining from the caught
UnsupportedAlgorithm) so unsupported-algorithm errors are wrapped the same way
as other PEM parse errors.
In `@src/omnibase_infra/runtime/service_runtime_host_process.py`:
- Around line 3729-3855: Compute a normalized correlation_id at the start of the
inbound handling (e.g., corr_id = envelope.get("correlation_id") or
str(envelope.get("trace_id")) or str(uuid4())) and import uuid4 from uuid; then
include this correlation_id in the extra dict of every rejection/logging call in
this block (the logger.warning calls around self._policy_engine.evaluate_inbound
rejection, the "Envelope signature validation failed" warning after
self._envelope_validator.validate_envelope, the "Failed to validate signed
envelope" exception handler, and the "Unsigned envelope rejected
(reject_unsigned=True)" warning), and also add it to the successful "Signed
envelope validated successfully" debug extra so all paths propagate
correlation_id consistently (reference symbols:
self._policy_engine.evaluate_inbound,
self._envelope_validator.validate_envelope, msg_envelope.trace_id, and the
logger.warning/logger.debug calls).
🧹 Nitpick comments (4)
pyproject.toml (1)
39-41: Stale inline comment after version bump.The comment at Line 39 still references "v0.14.0: Latest core release" but the dependency is now
^0.15.0. Update the comment to reflect the new version and its changelog (e.g., ModelMessageEnvelope support).📝 Suggested fix
-# omnibase-core v0.14.0: Latest core release -# (v0.13.1 included: Contract registration support) +# omnibase-core v0.15.0: Adds ModelMessageEnvelope for gateway signing +# (v0.14.0 included: Contract registration support) omnibase-core = "^0.15.0"tests/integration/gateway/test_policy_engine.py (1)
27-28: Missing@pytest.mark.integrationmarker.Per coding guidelines, integration tests should use the
@pytest.mark.integrationmarker for test classification. Add the marker to the test classes or module level.🔧 Suggested fix
+import pytest + +pytestmark = pytest.mark.integration + + class TestPolicyEngineTopicAllowlist: """Tests for topic allowlist enforcement."""Alternatively, add
@pytest.mark.integrationto each test class.As per coding guidelines: "Use pytest markers:
@pytest.mark.unit,@pytest.mark.integration,@pytest.mark.slow,@pytest.mark.chaos,@pytest.mark.serialfor test classification"tests/integration/gateway/test_gateway_config.py (1)
25-26: Missing@pytest.mark.integrationmarker.Per coding guidelines, integration tests should use the
@pytest.mark.integrationmarker. Add the marker at module level or to each test class.🔧 Suggested fix
+pytestmark = pytest.mark.integration + + class TestGatewayConfigRequired: """Tests for required configuration fields."""As per coding guidelines: "Use pytest markers:
@pytest.mark.unit,@pytest.mark.integration,@pytest.mark.slow,@pytest.mark.chaos,@pytest.mark.serialfor test classification"tests/integration/gateway/test_envelope_validator.py (1)
41-42: Missing@pytest.mark.integrationmarker.Per coding guidelines, integration tests should use the
@pytest.mark.integrationmarker. Add the marker at module level.🔧 Suggested fix
from .conftest import ModelTestPayload +pytestmark = pytest.mark.integration + class TestEnvelopeValidatorValidEnvelope: """Tests for valid envelope validation."""As per coding guidelines: "Use pytest markers:
@pytest.mark.unit,@pytest.mark.integration,@pytest.mark.slow,@pytest.mark.chaos,@pytest.mark.serialfor test classification"
| def evaluate_inbound( | ||
| self, | ||
| topic: str, | ||
| realm: str | None = None, | ||
| ) -> PolicyDecision: | ||
| """Evaluate inbound message policy. | ||
|
|
||
| Checks whether an inbound message should be allowed based on: | ||
| 1. Topic allowlist (if configured) | ||
| 2. Realm boundary enforcement (if configured) | ||
|
|
||
| Both checks must pass for the message to be allowed. Checks are | ||
| performed in order and short-circuit on first failure. | ||
|
|
||
| Args: | ||
| topic: The topic the message is arriving on. | ||
| realm: The realm identifier from the message metadata. | ||
| Required if expected_realm is configured. | ||
|
|
||
| Returns: | ||
| PolicyDecision with decision (ALLOW/DENY) and reason. | ||
| The reason is always populated for DENY decisions. | ||
|
|
||
| Example: | ||
| >>> engine = ServicePolicyEngine( | ||
| ... allowed_topics=["events.*"], | ||
| ... expected_realm="tenant-123", | ||
| ... ) | ||
| >>> decision = engine.evaluate_inbound( | ||
| ... topic="events.order.created", | ||
| ... realm="tenant-123", | ||
| ... ) | ||
| >>> if not decision: | ||
| ... raise PermissionError(decision.reason) | ||
| """ | ||
| # Check 1: Topic allowlist | ||
| if not self.is_topic_allowed(topic): | ||
| reason = f"Topic '{topic}' not in allowlist" | ||
| self._log_rejection("inbound", topic, reason) | ||
| return PolicyDecision( | ||
| decision=EnumPolicyDecision.DENY, | ||
| reason=reason, | ||
| ) | ||
|
|
||
| # Check 2: Realm boundary enforcement | ||
| if self._expected_realm is not None: | ||
| if realm != self._expected_realm: | ||
| reason = ( | ||
| f"Realm mismatch: expected '{self._expected_realm}', got '{realm}'" | ||
| ) | ||
| self._log_rejection("inbound", topic, reason) | ||
| return PolicyDecision( | ||
| decision=EnumPolicyDecision.DENY, | ||
| reason=reason, | ||
| ) |
There was a problem hiding this comment.
Include correlation_id in policy rejection logs
_log_rejection emits warnings without a correlation_id, and callers don’t propagate one. Add an optional correlation_id parameter to evaluate_inbound/evaluate_outbound and include it in the log extra payload so rejections are traceable.
🔧 Proposed fix
@@
-import logging
+import logging
+from uuid import UUID
@@
def evaluate_inbound(
self,
topic: str,
realm: str | None = None,
+ correlation_id: UUID | None = None,
) -> PolicyDecision:
@@
- self._log_rejection("inbound", topic, reason)
+ self._log_rejection("inbound", topic, reason, correlation_id)
@@
- self._log_rejection("inbound", topic, reason)
+ self._log_rejection("inbound", topic, reason, correlation_id)
@@
def evaluate_outbound(
self,
topic: str,
+ correlation_id: UUID | None = None,
) -> PolicyDecision:
@@
def _log_rejection(
self,
direction: str,
topic: str,
reason: str,
+ correlation_id: UUID | None = None,
) -> None:
@@
logger.warning(
"Policy rejection: %s message on topic '%s' - %s",
direction,
topic,
reason,
extra={
"direction": direction,
"topic": topic,
"reason": reason,
"policy_engine": "ServicePolicyEngine",
+ "correlation_id": str(correlation_id) if correlation_id else None,
},
)As per coding guidelines, "Propagate correlation_id from incoming requests; auto-generate with uuid4() if missing; include in all error context".
Also applies to: 383-418
🤖 Prompt for AI Agents
In `@src/omnibase_infra/gateway/services/service_policy_engine.py` around lines
223 - 277, Add an optional correlation_id parameter to evaluate_inbound (and
mirror the same change in evaluate_outbound) so callers can pass a trace id;
inside each function, if correlation_id is None generate one with uuid.uuid4();
when calling _log_rejection("inbound"/"outbound", topic, reason) include the
correlation_id in the log extra/context payload so the rejection warnings carry
the id; update any return paths that call _log_rejection to forward the
generated or provided correlation_id and ensure function signatures
(evaluate_inbound, evaluate_outbound) and any call sites are updated to
accept/propagate the new parameter.
Refactor _publish_envelope_safe to use concrete types instead of dict[str, object] | BaseModel union: - _serialize_envelope now accepts only dict[str, object] - _publish_envelope_safe now accepts only dict[str, object] - Add _publish_model_safe for BaseModel payloads with signing support - Update call sites to dispatch based on response type - Revert INFRA_MAX_UNIONS from 119 to 118 (no new union added) This maintains the union count at 118 while preserving full envelope signing functionality for BaseModel payloads.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@src/omnibase_infra/runtime/service_runtime_host_process.py`:
- Around line 2514-2526: The policy rejection log in the outbound check (inside
the method that calls self._policy_engine.evaluate_outbound(topic)) must include
a correlation_id: pull correlation_id from the current envelope (e.g.,
envelope.get("correlation_id") or envelope.correlation_id) and if missing create
one with uuid.uuid4().hex (or str(uuid.uuid4())), then add that correlation_id
to the logger.warning extra dict alongside "topic" and "reason" before
returning; reference the evaluate_outbound call, the logger.warning invocation,
and use uuid.uuid4() to generate when needed.
- Around line 3815-3853: When extracting the validated inner payload in the
block after self._envelope_validator.validate_envelope (working with
msg_envelope, payload, extracted_envelope, and model_dump), ensure the original
trace_id is propagated as correlation_id and if missing generate one with
uuid.uuid4(); specifically compute corr_id = str(msg_envelope.trace_id) if
present else str(uuid.uuid4()), then inject it into the extracted_envelope (use
extracted_envelope.setdefault("correlation_id", corr_id) for dict cases and
include the same key when constructing the {"payload": payload} fallback) so
downstream consumers always receive a correlation_id.
- Around line 2395-2409: BaseModel responses currently skip adding
correlation_id because _publish_model_safe is called without it; update
_publish_model_safe to accept an optional correlation_id parameter and ensure it
inserts correlation_id into the envelope_dict when absent (similar to the dict
branch), then call _publish_model_safe(response, self._output_topic,
correlation_id=correlation_id) from the handler path; adjust any signing or
trace_id extraction in _publish_model_safe to preserve behavior while ensuring
the final published envelope contains the correlation_id.
| # Check outbound policy (if policy engine configured) | ||
| if self._policy_engine is not None: | ||
| decision = self._policy_engine.evaluate_outbound(topic) | ||
| if not decision: | ||
| logger.warning( | ||
| "Outbound message rejected by policy", | ||
| extra={ | ||
| "topic": topic, | ||
| "reason": decision.reason, | ||
| }, | ||
| ) | ||
| return | ||
|
|
There was a problem hiding this comment.
Add correlation_id to outbound policy rejection logs.
Policy rejections are warnings but currently omit correlation_id. Please normalize (or generate) correlation_id from the envelope and include it in the warning extra.
Suggested fix
- if self._policy_engine is not None:
+ if self._policy_engine is not None:
+ correlation_id = normalize_correlation_id(envelope.get("correlation_id"))
decision = self._policy_engine.evaluate_outbound(topic)
if not decision:
logger.warning(
"Outbound message rejected by policy",
extra={
"topic": topic,
"reason": decision.reason,
+ "correlation_id": str(correlation_id),
},
)
returnAs per coding guidelines, "Propagate correlation_id from incoming requests; auto-generate with uuid4() if missing; include in all error context".
🤖 Prompt for AI Agents
In `@src/omnibase_infra/runtime/service_runtime_host_process.py` around lines 2514
- 2526, The policy rejection log in the outbound check (inside the method that
calls self._policy_engine.evaluate_outbound(topic)) must include a
correlation_id: pull correlation_id from the current envelope (e.g.,
envelope.get("correlation_id") or envelope.correlation_id) and if missing create
one with uuid.uuid4().hex (or str(uuid.uuid4())), then add that correlation_id
to the logger.warning extra dict alongside "topic" and "reason" before
returning; reference the evaluate_outbound call, the logger.warning invocation,
and use uuid.uuid4() to generate when needed.
- Add correlation_id tracking throughout gateway publish flow - Implement sign_dict() for dict envelope signing (parity with models) - Add UnsupportedAlgorithm exception handling in key loader - Include correlation_id in policy rejection and gateway rejection logs - Preserve trace_id as correlation_id when extracting signed payloads - Add @pytest.mark.integration markers to gateway test files - Update stale version comment in pyproject.toml - Add 7 new tests for sign_dict functionality - Add validation exemption for sign_dict parameters
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/omnibase_infra/gateway/services/service_policy_engine.py`:
- Around line 224-277: The evaluate_inbound method currently logs rejections
without a correlation_id when correlation_id is None; update evaluate_inbound to
ensure a correlation id exists by generating one with uuid.uuid4() (or UUID(...)
as appropriate) at the start of the rejection path and pass that generated id
into _log_rejection and any returned denial context; modify the code paths in
evaluate_inbound (the realm mismatch branch and the topic allowlist branch) to
set correlation_id = correlation_id or uuid.uuid4() before calling
self._log_rejection, and add the necessary import for uuid.uuid4 at the top if
missing so all rejection logs include a traceable correlation_id.
🧹 Nitpick comments (1)
tests/integration/gateway/test_envelope_validator.py (1)
17-40: Consider movingpytestmarkafter all imports.The
pytestmark = pytest.mark.integrationassignment on line 23 is placed between import groups, which is unconventional. Standard Python style places all imports first, then module-level assignments.🧹 Suggested reordering
from __future__ import annotations import base64 import pytest +from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PrivateKey, + Ed25519PublicKey, +) + +from omnibase_core.models.envelope.model_envelope_signature import ( + ModelEnvelopeSignature, +) +from omnibase_core.models.envelope.model_message_envelope import ModelMessageEnvelope +from omnibase_infra.gateway import ( + EnumValidationErrorCode, + ServiceEnvelopeSigner, + ServiceEnvelopeValidator, + ValidationResult, +) + +from .conftest import ModelTestPayload pytestmark = pytest.mark.integration -from cryptography.hazmat.primitives.asymmetric.ed25519 import ( - Ed25519PrivateKey, - Ed25519PublicKey, -) - -from omnibase_core.models.envelope.model_envelope_signature import ( - ModelEnvelopeSignature, -) -from omnibase_core.models.envelope.model_message_envelope import ModelMessageEnvelope -from omnibase_infra.gateway import ( - EnumValidationErrorCode, - ServiceEnvelopeSigner, - ServiceEnvelopeValidator, - ValidationResult, -) - -from .conftest import ModelTestPayload
- Auto-generate correlation_id in policy engine rejection logs - Add correlation_id to unsigned envelope acceptance logs - Add trace_id to _publish_model_safe error logging - Move pytestmark after imports in test files (4 files) - Fix docstring formatting (D413 blank line violations)
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/omnibase_infra/gateway/services/service_envelope_validator.py`:
- Around line 281-433: The validation code omits correlation_id in its log
extras; compute a normalized correlation_id at the start of validation (e.g.,
corr_id = str(envelope.correlation_id) if present else str(uuid4())) and include
"correlation_id": corr_id in the extra dict for every logging call that reports
failures or warnings (realm mismatch in the realm check, missing signature path
and the unsigned-accepted debug in the signature check, unknown signer, payload
hash mismatch, signature verification failure and both exception handlers in the
Ed25519 verification block). Update service_envelope_validator.py
functions/methods that reference envelope (e.g., the validator method containing
checks and variables signer, public_key, computed_hash, message, signature_b64)
to import uuid4 and use the single computed corr_id variable in all
logger.*(..., extra={...}) payloads for consistent propagation and
auto-generation when missing.
…arkers [OMN-1899] Address remaining PR #249 review feedback: - Add correlation_id parameter to evaluate_outbound() in policy engine - Include correlation_id in all envelope validation failure logs - Pass correlation_id to evaluate_outbound() in runtime publish methods - Extract correlation_id from envelope or auto-generate in gateway validation - Move pytestmark after all imports in test_policy_engine.py
…alidation [OMN-1899] - Fix model_dump(mode="python") to mode="json" to prevent datetime serialization failure when gateway signing is enabled - Add threading.Lock to ServiceEnvelopeValidator._public_keys for thread-safe signer mutation during concurrent access - Add absolute path validation for key paths in ModelGatewayConfig - Add private key file permission warning in util_key_loader - Use logger.exception for signing failures (TRY400 compliance) - Remove invalid noqa directive on runtime_id field - Add key loader and runtime gateway integration tests (45 tests) Review iteration: 1/10
…ustness [OMN-1899] - Document reject_unsigned dependency on public_key_path being configured - Add structural discriminator for signed envelope detection: verify signature is a dict with algorithm key before parsing as signed Review iteration: 2/10
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@src/omnibase_infra/gateway/utils/util_key_loader.py`:
- Around line 89-138: The warning/exception log calls in util_key_loader.py are
including context.correlation_id only in the formatted message but not in the
structured `extra` payload; update all logger.warning and logger.exception calls
in the key-loading paths (the block that checks file permissions, the read_bytes
OSError handler, and the PEM parsing/UnsupportedAlgorithm handlers around
load_pem_private_key) to include "correlation_id": context.correlation_id inside
the extra dict so structured logs can be correlated; do the same change for the
other loader block referenced (the similar handlers around lines 194-224) and
ensure the ProtocolConfigurationError raises remain unchanged except for logging
extras.
In `@src/omnibase_infra/runtime/service_runtime_host_process.py`:
- Around line 3971-4000: The unsigned-envelope branch currently re-extracts
correlation_id into variables cid/cid_str and may log None; instead reuse the
already-normalized correlation id that was computed earlier (the
normalized/generated correlation_id variable used across the function, e.g.,
correlation_id or normalized_correlation_id) in both logger.warning and
logger.debug extra contexts; remove or stop using cid/cid_str for logging and
ensure the normalized id (which will be uuid4() if auto-generated) is passed as
"correlation_id" so tracing is consistent.
- Around line 2599-2695: In _publish_model_safe, when signing is disabled
(self._envelope_signer is None) or signing fails (except block) ensure the
outgoing envelope_dict includes a correlation_id: if correlation_id arg (or
extracted trace_id) is present, inject it into envelope_dict (as a string); if
neither exists, generate one with uuid.uuid4() and set it. Update both the
except signing-failure branch (where envelope_dict = model.model_dump(...)) and
the "no signer" branch (where envelope_dict = model.model_dump(...)) to perform
this fallback injection before calling self._serialize_envelope and publishing;
use the uuid module for generation and reference _publish_model_safe,
envelope_dict, correlation_id/trace_id, and _serialize_envelope to locate the
changes.
…ze unsigned paths [OMN-1899] - Add correlation_id to all structured `extra` dicts in key loader logs - Remove redundant cid/cid_str extraction in unsigned envelope branch, reuse normalized correlation_id already computed at function scope - Inject correlation_id into unsigned BaseModel envelope dicts for tracing parity with signed path
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@src/omnibase_infra/runtime/service_runtime_host_process.py`:
- Around line 3739-3813: The gateway init failure logs in
_initialize_gateway_from_config call logger.warning without a correlation_id;
update both warning calls (the "Invalid gateway config type" and the final
"Failed to parse gateway config, proceeding without") to include a
correlation_id in extra (use self._correlation_id if present else generate one
with uuid.uuid4()), and ensure uuid is imported; keep the rest of the extra
dicts intact and include the correlation_id key so all gateway initialization
warning logs include tracing context (references:
_initialize_gateway_from_config, gateway_raw, ModelGatewayConfig,
self._initialize_gateway, logger.warning).
- Around line 2521-2547: The code currently reads correlation_id from envelope
into local variable correlation_id (via cid) but doesn't generate or propagate
one when missing; update the logic so that if correlation_id is None after the
UUID parsing, create a new UUID with uuid.uuid4(), assign it to correlation_id
and also write it back into the envelope (envelope["correlation_id"] =
str(correlation_id) or UUID as appropriate) before calling
self._policy_engine.evaluate_outbound and before any signing/publishing code;
ensure the logger.warning extra uses the generated correlation_id string and any
downstream signing/publish functions read the correlation_id from the envelope
or the local correlation_id variable so the same id is propagated.
- Around line 3892-3999: The code lets unsigned envelopes through when
self._envelope_validator is None even if self._gateway_config.reject_unsigned is
True; fix by enforcing reject_unsigned regardless of validator availability:
either add config-time validation (in ModelGatewayConfig or the service runtime
host initializer) that raises an error if gateway_config.reject_unsigned is True
while public_key_path is None/empty, or modify the envelope-processing logic to
check if self._envelope_validator is None and self._gateway_config and
self._gateway_config.reject_unsigned and then log and return None (reject)
before returning envelope; reference symbols: self._envelope_validator,
self._gateway_config.reject_unsigned, public_key_path, and ModelGatewayConfig.
| except Exception as e: | ||
| # Gateway initialization failure is non-fatal - log and continue | ||
| # The system will operate without signing/validation | ||
| logger.warning( | ||
| "Failed to initialize gateway, proceeding without", | ||
| extra={ | ||
| "error": str(e), | ||
| "realm": config.realm, | ||
| "runtime_id": config.runtime_id, | ||
| }, | ||
| ) | ||
| # Clear any partially initialized components | ||
| self._gateway_config = None | ||
| self._envelope_signer = None | ||
| self._envelope_validator = None | ||
| self._policy_engine = None | ||
|
|
||
| async def _initialize_gateway_from_config(self) -> None: | ||
| """Initialize gateway from runtime configuration. | ||
|
|
||
| Reads gateway configuration from the runtime config dict and initializes | ||
| gateway components. Called during start() to enable envelope signing, | ||
| validation, and policy enforcement. | ||
|
|
||
| Configuration keys: | ||
| - gateway.enabled: bool (default: True) | ||
| - gateway.realm: str (required if enabled) | ||
| - gateway.runtime_id: str (required if enabled) | ||
| - gateway.private_key_path: str | None (optional, enables signing) | ||
| - gateway.public_key_path: str | None (optional, enables validation) | ||
| - gateway.allowed_topics: list[str] (optional, topic allowlist) | ||
| - gateway.reject_unsigned: bool (default: True) | ||
|
|
||
| Example config: | ||
| { | ||
| "gateway": { | ||
| "enabled": True, | ||
| "realm": "dev", | ||
| "runtime_id": "runtime-dev-001", | ||
| "private_key_path": "/etc/onex/keys/private.pem", | ||
| "public_key_path": "/etc/onex/keys/public.pem", | ||
| "allowed_topics": ["events.*", "commands.*"], | ||
| "reject_unsigned": True, | ||
| } | ||
| } | ||
| """ | ||
| if self._config is None: | ||
| logger.debug("No runtime config provided, skipping gateway setup") | ||
| return | ||
|
|
||
| gateway_raw = self._config.get("gateway") | ||
| if gateway_raw is None: | ||
| logger.debug("Gateway not configured, skipping") | ||
| return | ||
|
|
||
| try: | ||
| if isinstance(gateway_raw, dict): | ||
| gateway_config = ModelGatewayConfig.model_validate(gateway_raw) | ||
| elif isinstance(gateway_raw, ModelGatewayConfig): | ||
| gateway_config = gateway_raw | ||
| else: | ||
| logger.warning( | ||
| "Invalid gateway config type", | ||
| extra={"type": type(gateway_raw).__name__}, | ||
| ) | ||
| return | ||
|
|
||
| # Call the synchronous initialization method | ||
| self._initialize_gateway(gateway_config) | ||
|
|
||
| except Exception as e: | ||
| logger.warning( | ||
| "Failed to parse gateway config, proceeding without", | ||
| extra={"error": str(e)}, | ||
| ) |
There was a problem hiding this comment.
Add correlation_id to gateway init failure logs.
Gateway init failures are warnings but currently lack correlation_id in structured extras, which weakens tracing.
🛠️ Proposed fix
def _initialize_gateway(self, config: ModelGatewayConfig) -> None:
+ correlation_id = uuid4()
...
except Exception as e:
logger.warning(
"Failed to initialize gateway, proceeding without",
extra={
"error": str(e),
"realm": config.realm,
"runtime_id": config.runtime_id,
+ "correlation_id": str(correlation_id),
},
)
@@
async def _initialize_gateway_from_config(self) -> None:
+ correlation_id = uuid4()
...
else:
logger.warning(
"Invalid gateway config type",
- extra={"type": type(gateway_raw).__name__},
+ extra={
+ "type": type(gateway_raw).__name__,
+ "correlation_id": str(correlation_id),
+ },
)
return
@@
except Exception as e:
logger.warning(
"Failed to parse gateway config, proceeding without",
- extra={"error": str(e)},
+ extra={"error": str(e), "correlation_id": str(correlation_id)},
)As per coding guidelines, "Propagate correlation_id from incoming requests; auto-generate with uuid4() if missing; include in all error context".
🤖 Prompt for AI Agents
In `@src/omnibase_infra/runtime/service_runtime_host_process.py` around lines 3739
- 3813, The gateway init failure logs in _initialize_gateway_from_config call
logger.warning without a correlation_id; update both warning calls (the "Invalid
gateway config type" and the final "Failed to parse gateway config, proceeding
without") to include a correlation_id in extra (use self._correlation_id if
present else generate one with uuid.uuid4()), and ensure uuid is imported; keep
the rest of the extra dicts intact and include the correlation_id key so all
gateway initialization warning logs include tracing context (references:
_initialize_gateway_from_config, gateway_raw, ModelGatewayConfig,
self._initialize_gateway, logger.warning).
Resolve merge conflict in service_runtime_host_process.py __init__, keeping both gateway components (OMN-1899) and introspection service (OMN-1930).
…tion_id to init logs [OMN-1899] - Move reject_unsigned check outside envelope_validator block so it is enforced even when public_key_path is not configured - Auto-generate correlation_id in _publish_envelope_safe when missing - Add correlation_id to gateway init failure warning logs - Update stale v0.14.0 comment in pyproject.toml
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@src/omnibase_infra/runtime/service_runtime_host_process.py`:
- Around line 2718-2739: In _publish_model_safe, ensure trace_id is never None
by auto-generating a UUID via uuid.uuid4() when both correlation_id arg and
model.correlation_id are missing; set trace_id to this generated UUID, propagate
it back into the model if the model has a writable correlation_id attribute, and
use that trace_id for the policy check (_policy_engine.evaluate_outbound),
subsequent signing, and all logger.extra contexts (replace existing nullable
uses of trace_id with the guaranteed stringified trace_id).
- Around line 4065-4078: The code extracts or generates a correlation_id but
never writes it back to the envelope; update the block where correlation_id is
set (the variables named correlation_id, cid and the uuid4() fallback in
service_runtime_host_process.py) to assign the resolved ID into the envelope
(e.g., envelope["correlation_id"] = <resolved id>), ensuring the stored form
matches expected envelope usage (string if the envelope expects strings or UUID
otherwise), and ensure subsequent error/context logging uses that same envelope
value so downstream processing and error contexts see the propagated
correlation_id.
- Around line 4132-4137: The current validation branch only runs when
_envelope_validator is present and has_signature is true, allowing attackers to
set has_signature=True while _envelope_validator is None to bypass
reject_unsigned; update the condition and handling so that
signed-but-unverifiable envelopes are treated as invalid (or as unsigned) by
explicitly checking for has_signature when _envelope_validator is None and
calling the existing reject_unsigned (or the invalid path). Specifically, modify
the block around _envelope_validator, has_signature, and has_required_fields to:
if has_signature and _envelope_validator is None -> reject or mark as unsigned
using the same code path as reject_unsigned; otherwise keep the current flow
that invokes _envelope_validator when present.
…n handling - Fixed: service_runtime_host_process.py:4193 - Narrowed broad `except Exception` to `(ValidationError, ValueError, KeyError, TypeError)` to avoid silently swallowing real bugs like ImportError or AttributeError - Fixed: test_gateway_config.py - Added TestKeyPathAbsoluteValidation class with 6 tests covering path traversal prevention in _validate_key_path_is_absolute - Fixed: test_runtime_gateway_integration.py - Added 9 tests covering inbound _validate_gateway_envelope: reject_unsigned enforcement (including regression test for commit 4e2181d), policy engine checks, realm mismatch, signed envelope validation, invalid signatures, and malformed envelope handling - Fixed: service_envelope_validator.py:563 - Added type annotation to __all__ for consistency with other gateway modules Review iteration: 1/10
…ce_id, propagate correlation_id [OMN-1899] - Reject signed envelopes when no validator is configured (fail-closed security: forged signatures must not be silently accepted) - Auto-generate trace_id in _publish_model_safe when missing, including string correlation_id parsing from model attributes - Inject auto-generated correlation_id into inbound envelopes for downstream handler tracing Addresses remaining PR #249 review feedback.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/omnibase_infra/runtime/service_runtime_host_process.py`:
- Around line 2783-2790: The logger.exception call in the signing-failure path
currently only includes trace_id; update the error context to also include
correlation_id by retrieving it from the current request/envelope (or generating
one with uuid.uuid4() when missing) and add "correlation_id":
str(correlation_id) to the extra dict passed to logger.exception (alongside
topic and trace_id) so correlation IDs are propagated for this error path;
ensure you import uuid where needed and use the same correlation_id value used
elsewhere in functions handling outbound envelopes.
- Around line 4194-4207: When building extracted_envelope in
service_runtime_host_process, ensure correlation_id is always propagated: if
msg_envelope.trace_id is present set extracted_envelope["correlation_id"] =
msg_envelope.trace_id, otherwise fall back to msg_envelope.correlation_id if
available, and if neither exists generate a new UUID via uuid.uuid4() and assign
that value; update the logic around msg_envelope, trace_id and
extracted_envelope to perform this fallback path and add an import for uuid
where needed so every outgoing envelope has a normalized correlation_id.
| # Signing failure is non-fatal - log and publish unsigned | ||
| logger.exception( | ||
| "Failed to sign outbound envelope, publishing unsigned", | ||
| extra={ | ||
| "topic": topic, | ||
| "trace_id": str(trace_id) if trace_id else None, | ||
| }, | ||
| ) |
There was a problem hiding this comment.
Add correlation_id to signing-failure log context.
The exception log only includes trace_id; error contexts should always carry correlation_id for tracing.
🛠️ Proposed fix
logger.exception(
"Failed to sign outbound envelope, publishing unsigned",
extra={
"topic": topic,
- "trace_id": str(trace_id) if trace_id else None,
+ "correlation_id": str(trace_id) if trace_id else None,
+ "trace_id": str(trace_id) if trace_id else None,
},
)As per coding guidelines, "Propagate correlation_id from incoming requests; auto-generate with uuid4() if missing; include in all error context".
🤖 Prompt for AI Agents
In `@src/omnibase_infra/runtime/service_runtime_host_process.py` around lines 2783
- 2790, The logger.exception call in the signing-failure path currently only
includes trace_id; update the error context to also include correlation_id by
retrieving it from the current request/envelope (or generating one with
uuid.uuid4() when missing) and add "correlation_id": str(correlation_id) to the
extra dict passed to logger.exception (alongside topic and trace_id) so
correlation IDs are propagated for this error path; ensure you import uuid where
needed and use the same correlation_id value used elsewhere in functions
handling outbound envelopes.
| # The payload could be a dict or a BaseModel | ||
| payload = msg_envelope.payload | ||
| if isinstance(payload, dict): | ||
| extracted_envelope = dict(payload) # Copy to avoid mutation | ||
| elif hasattr(payload, "model_dump"): | ||
| extracted_envelope = payload.model_dump(mode="json") | ||
| else: | ||
| # Unknown payload type, use as-is | ||
| extracted_envelope = {"payload": payload} | ||
|
|
||
| # Preserve trace_id as correlation_id for downstream tracking | ||
| if msg_envelope.trace_id is not None: | ||
| extracted_envelope["correlation_id"] = msg_envelope.trace_id | ||
|
|
There was a problem hiding this comment.
Preserve correlation_id when signed envelope lacks trace_id.
If trace_id is missing, the extracted payload can lose correlation_id even though one was generated earlier. Ensure a fallback to the normalized correlation_id.
🛠️ Proposed fix
- if msg_envelope.trace_id is not None:
- extracted_envelope["correlation_id"] = msg_envelope.trace_id
+ if msg_envelope.trace_id is not None:
+ extracted_envelope["correlation_id"] = msg_envelope.trace_id
+ else:
+ extracted_envelope.setdefault("correlation_id", correlation_id)As per coding guidelines, "Propagate correlation_id from incoming requests; auto-generate with uuid4() if missing; include in all error context".
🤖 Prompt for AI Agents
In `@src/omnibase_infra/runtime/service_runtime_host_process.py` around lines 4194
- 4207, When building extracted_envelope in service_runtime_host_process, ensure
correlation_id is always propagated: if msg_envelope.trace_id is present set
extracted_envelope["correlation_id"] = msg_envelope.trace_id, otherwise fall
back to msg_envelope.correlation_id if available, and if neither exists generate
a new UUID via uuid.uuid4() and assign that value; update the logic around
msg_envelope, trace_id and extracted_envelope to perform this fallback path and
add an import for uuid where needed so every outgoing envelope has a normalized
correlation_id.
…ency and test markers [OMN-1899] - Simplify redundant correlation_id conditionals in _publish_envelope_safe (guaranteed non-None after auto-generation) - Add correlation_id to signing success/failure log extras in _publish_model_safe - Preserve correlation_id fallback when signed envelope lacks trace_id - Add pytestmark = pytest.mark.integration to test_runtime_introspection - Fix versionadded directive to use semver (0.4.1) instead of ticket ID
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/omnibase_infra/runtime/service_runtime_host_process.py`:
- Around line 2605-2666: The envelope must be serialized to convert UUIDs to
strings before signing to avoid json.dumps serialization failures; update the
signing block in the method using self._envelope_signer.sign_dict to call
self._serialize_envelope(envelope) (or equivalent) and pass that serialized dict
as payload to _envelope_signer.sign_dict (ensure correlation_id in the
serialized envelope is a string), then use the returned signed model_dump as
final_envelope; reference _serialize_envelope, _envelope_signer.sign_dict, and
hash_canonical_json in your changes.
…TypeError [OMN-1899] hash_canonical_json uses json.dumps which raises TypeError on UUID objects. Previously serialization only happened after signing, causing sign_dict to silently fail and degrade to unsigned publish. Now _serialize_envelope is called before sign_dict so UUID fields are converted to strings first.
…e inconsistency [OMN-1899] - Updated reject_unsigned docstring and field description to reflect actual runtime behavior (enforced without public_key_path) - Replaced fnmatch.fnmatch() with fnmatch.fnmatchcase() in policy engine for consistent case-sensitive topic matching across all platforms - Normalized correlation_id assignment to str() in both branches of _validate_gateway_envelope to prevent mixed UUID/str types Review iteration: 1/10
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/omnibase_infra/gateway/models/model_gateway_config.py`:
- Around line 126-129: The allowed_topics Field currently expects a tuple[str,
...] but with Pydantic v2 strict mode list inputs (e.g.,
allowed_topics=["events.*", "commands.*"]) will fail; add a Pydantic v2 field
validator on "allowed_topics" (use field_validator('allowed_topics',
mode='before') or mode='before') to coerce incoming list inputs to a tuple (and
pass tuples/None through unchanged) inside the ModelGatewayConfig (or the class
that defines allowed_topics) so list-style configuration shown in the docstring
validates correctly.
- Around line 140-162: The validator _validate_key_path_is_absolute must return
Path objects (or None) not the original input; update it to coerce v into a
pathlib.Path (e.g., path = Path(v) if v is not None and not isinstance(v, Path)
else v), validate path.is_absolute(), and then return that Path instance (or
None) so Pydantic strict=True type checks pass; adjust the method in class-level
validator decorated on "private_key_path" and "public_key_path" accordingly.
…OMN-1899] Coerce list→tuple for allowed_topics, return Path objects from key-path validator, and move pytestmark after imports in test file.
Summary
Implement runtime gateway functionality that signs outbound envelopes and validates inbound envelopes, enforcing realm boundaries per the ONEX security architecture.
ModelGatewayConfigwith realm, runtime_id, Ed25519 keypair paths, topic allowlistServiceEnvelopeSignerwraps payloads inModelMessageEnvelopewith Ed25519 signaturesServiceEnvelopeValidatorvalidates signatures, detects tampering, enforces realm boundariesServicePolicyEngineenforces topic allowlist with wildcard supportArchitecture
Key Rule: Messages never cross realms directly. They cross only via runtime gateways with explicit policy.
Components Created
ModelGatewayConfiggateway/models/ServiceEnvelopeSignergateway/services/ServiceEnvelopeValidatorgateway/services/ServicePolicyEnginegateway/services/util_key_loadergateway/utils/Runtime Integration
RuntimeHostProcess.start()_publish_envelope_safe()_handle_envelope()Dependencies
omnibase-coreto 0.15.0 forModelMessageEnvelopeblake3for cryptographic hashing (transitive from core)Test Plan
poetry run pytest tests/integration/gateway/ -vpoetry run mypy src/omnibase_infra/gateway/poetry run ruff check src/omnibase_infra/gateway/Blocked By
OMN-1898✅ (ModelMessageEnvelope in omnibase-core 0.15.0)Linear
Closes OMN-1899
Summary by CodeRabbit
New Features
Tests
Chores
Bug Fixes