Skip to content

feat(runtime): complete Vault integration for SecretResolver [OMN-1374] - #167

Merged
jonahgabriel merged 13 commits into
mainfrom
jonah/omn-1374-secretresolver-complete-vault-integration-and-add
Jan 18, 2026
Merged

jonahgabriel merged 13 commits into
mainfrom
jonah/omn-1374-secretresolver-complete-vault-integration-and-add

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Jan 17, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Vault Integration: Full implementation of _resolve_vault_secret() via HandlerVault with path parsing (mount/path#field format)
  • Correlation ID Propagation: Added optional correlation_id parameter to get_secret() and get_secret_async() for distributed tracing
  • Metrics/Observability: Resolution metrics tracking (success/failure counts by source type, cache hits, latency) with external metrics collector support

Changes

src/omnibase_infra/runtime/secret_resolver.py

  • Implemented _resolve_vault_secret() with HandlerVault integration
  • Added path parsing for mount/path#field format
  • Added correlation_id parameter propagation through resolution chain
  • Added get_resolution_metrics() method for observability
  • Added set_metrics_collector() for external metrics integration
  • Graceful degradation when vault_handler is None

tests/unit/runtime/test_secret_resolver.py

  • Updated existing Vault tests to use mocked handler
  • Added TestSecretResolverVaultIntegration class (path parsing, error handling)
  • Added TestSecretResolverCorrelationId class (ID propagation verification)
  • Added TestSecretResolverMetrics class (metrics tracking, external collector)

tests/integration/runtime/test_secret_resolver_vault_integration.py (new)

  • Real Vault integration tests with proper test isolation
  • Tests for secret resolution, caching, correlation ID, metrics
  • Graceful skip when Vault is unavailable (CI-friendly)

Test Plan

  • Unit tests pass: pytest tests/unit/runtime/test_secret_resolver.py -v
  • Integration tests pass (with Vault): pytest tests/integration/runtime/test_secret_resolver_vault_integration.py -v
  • Integration tests skip gracefully without Vault
  • Pre-commit hooks pass (ruff, mypy, ONEX validators)
  • Verify correlation ID appears in error context when resolution fails
  • Verify metrics are tracked correctly for vault/env/file sources

Summary by CodeRabbit

  • New Features
    • Full Vault secret support (sync/async) with per-key locking, correlation ID propagation, pluggable metrics protocol, public metrics get/set, and a new metrics model export.
  • Bug Fixes
    • Improved path parsing/validation, stronger error handling and sanitized logging, cache TTL jitter to reduce stampedes, and graceful fallback when Vault is unavailable.
  • Tests
    • Added extensive unit and integration tests for Vault resolution, parsing, caching, metrics, correlation propagation, and failure scenarios.

✏️ Tip: You can customize this high-level summary in your review settings.

Implements production-ready Vault integration with observability:

- Full Vault integration via HandlerVault (path parsing: mount/path#field)
- Correlation ID propagation on get_secret() and get_secret_async()
- Metrics tracking: success/failure counts, cache hits, latency
- External metrics collector support via set_metrics_collector()
- Graceful degradation when vault_handler is None

Includes comprehensive unit tests for all features and integration tests
for real Vault scenarios with proper test isolation and cleanup.
@linear

linear Bot commented Jan 17, 2026

Copy link
Copy Markdown

OMN-1374

@coderabbitai

coderabbitai Bot commented Jan 17, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@jonahgabriel has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 18 minutes and 35 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between e644216 and 6ce290f.

📒 Files selected for processing (3)
  • src/omnibase_infra/runtime/secret_resolver.py
  • tests/integration/runtime/test_secret_resolver_vault_contract.py
  • tests/unit/runtime/test_secret_resolver.py
📝 Walkthrough

Walkthrough

SecretResolver now implements Vault KV v2 resolution with mount/path#field parsing, per-key async locks and LRU eviction, cache TTL jitter, correlation_id propagation, metrics hooks (ProtocolSecretResolverMetrics), improved error/logging, and accompanying unit and integration tests for Vault flows and metrics.

Changes

Cohort / File(s) Summary
Core SecretResolver Implementation
src/omnibase_infra/runtime/secret_resolver.py
Adds Vault KV v2 handling (path parsing _parse_vault_path_components), async/sync read flows, per-key asyncio.Lock + LRU eviction and DoS warnings, cache TTL jitter, correlation_id propagation, observability hooks (ProtocolSecretResolverMetrics), public metrics API (set_metrics_collector, get_resolution_metrics), error mapping and hardened logging, and updates __all__.
Metrics Model & Exports
src/omnibase_infra/runtime/models/model_secret_resolver_metrics.py , src/omnibase_infra/runtime/models/__init__.py
New immutable Pydantic ModelSecretResolverMetrics with success/failure counts, latency, cache hits/misses and derived properties (cache_hit_rate, total_resolutions); exported via package __all__.
Integration Tests (Vault)
tests/integration/runtime/test_secret_resolver_vault_integration.py
New live-Vault integration tests with gating, fixtures (unique_secret_path, cleanup_secret, write_test_secret); cover field vs full-secret reads, not-found cases, caching, correlation_id propagation, metrics, and graceful degradation when handler is None.
Unit Tests (Resolver, Metrics, Correlation)
tests/unit/runtime/test_secret_resolver.py
Replaces stubbed Vault tests with mocked sync/async Vault handler flows (MagicMock/AsyncMock), adds correlation_id propagation tests, metrics/collector interaction tests (collector errors, post-construction assignment), and adjusts path-parsing expectations.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant SR as SecretResolver
    participant Vault as HandlerVault
    participant Metrics as MetricsCollector

    Client->>SR: get_secret(logical_name, correlation_id)
    activate SR

    rect rgba(200,150,255,0.5)
    Note over SR: Check cache (hit/miss)
    end

    alt Cache miss
        SR->>SR: Parse vault path (mount/path#field)
        SR->>SR: Acquire per-key lock
        SR->>Vault: read_secret_kv2(mount, path, correlation_id)
        activate Vault
        Vault-->>SR: secret payload / not_found / error
        deactivate Vault

        rect rgba(150,200,255,0.5)
        Note over SR: Extract field, normalize to SecretStr, update cache (TTL + jitter)
        end

        SR->>Metrics: record latency, success/failure, cache_miss
        activate Metrics
        Metrics-->>SR: ack
        deactivate Metrics

        SR->>SR: Release per-key lock
    else Cache hit
        SR->>Metrics: record cache_hit
    end

    SR-->>Client: SecretStr or None
    deactivate SR
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I dug a path with careful paws,

I parsed the vault and read the laws,
Locks in place and metrics bright,
Caches hummed through day and night,
I hopped away with secrets snug and small.


Comment @coderabbitai help to get the list of available commands and usage tips.

@claude

claude Bot commented Jan 17, 2026

Copy link
Copy Markdown

Code Review: Vault Integration for SecretResolver [OMN-1374]

Summary

This PR implements complete Vault integration for the SecretResolver with correlation ID propagation and observability metrics. The implementation is solid overall with comprehensive test coverage.

✅ Strengths

1. Excellent Test Coverage

  • Unit tests: Comprehensive mocking-based tests covering path parsing, error handling, and edge cases
  • Integration tests: Real Vault integration with proper test isolation and graceful CI degradation
  • 565+ lines of new unit tests and 400+ lines of integration tests

2. Strong Security Practices

  • Vault paths never logged (prevents structure disclosure)
  • Proper error sanitization with correlation IDs
  • Path traversal protection maintained
  • Bootstrap secret isolation preserved

3. Robust Error Handling

  • Infrastructure errors properly propagated (InfraAuthenticationError, InfraTimeoutError, InfraUnavailableError)
  • Generic errors wrapped in SecretResolutionError
  • Graceful degradation when vault_handler is None

4. Observability Features

  • Built-in metrics tracking (success/failure counts, latency)
  • External metrics collector support via ProtocolSecretResolverMetrics
  • Structured logging with correlation IDs
  • Metrics failures never affect resolution (fail-safe design)

5. Documentation Quality

  • Comprehensive module docstring with examples
  • Clear path format documentation (mount/path#field)
  • Security considerations well-documented
  • Updated from stub placeholder to full implementation guide

🔍 Code Quality Observations

1. Type Safety ✅

  • No Any type violations detected
  • Proper use of object for generic envelope payloads
  • Strong typing throughout implementation

2. ONEX Pattern Compliance ✅

  • Follows container-based dependency injection pattern
  • Error handling uses ModelInfraErrorContext.with_correlation()
  • Correlation ID propagation properly implemented
  • Metrics protocol uses duck typing (not isinstance checks)

3. Thread Safety ✅

  • Proper locking strategy for sync/async coordination
  • Check-before-write pattern for cache updates
  • Per-key async locks for concurrent resolution

🚨 Potential Issues

1. Sync Method in Async Context ⚠️

Location: secret_resolver.py:1276-1293

The _read_vault_secret_sync method raises an error if called from an async context:

try:
    asyncio.get_running_loop()
    # Raises SecretResolutionError
except RuntimeError:
    pass  # Safe to use asyncio.run()

Issue: This could surprise users who might inadvertently call get_secret() (sync) from async code. While the error message is clear, this might be a common pitfall.

Recommendation: Consider adding a prominent warning in the docstring for get_secret() about this limitation, or document it in the class docstring.

2. Metrics Latency Sample Limit 🤔

Location: secret_resolver.py:701-704

if len(self._resolution_latencies) >= 1000:
    self._resolution_latencies.pop(0)  # Remove oldest
self._resolution_latencies.append((source_type, latency_ms))

Issue: Using pop(0) on a list is O(n) - could become a performance bottleneck with frequent secret resolutions.

Recommendation: Use collections.deque with maxlen=1000 instead:

from collections import deque
self._resolution_latencies: deque[tuple[str, float]] = deque(maxlen=1000)

This makes the rotation O(1) instead of O(n).

3. Path Parsing Edge Case 🔍

Location: secret_resolver.py:1488-1490

if len(parts) == 1:
    # No slash - entire path is the mount_point (edge case)
    return parts[0], "", field

Question: Is mount_point="secret", path="", field="password" a valid Vault path format? This seems like an unusual edge case that might not be supported by Vault's KV v2 engine.

Recommendation: Consider adding a validation check or test case to verify this edge case behavior with real Vault.

📝 Minor Suggestions

1. Import Organization (Nitpick)

Location: secret_resolver.py:148

The from typing import Protocol, runtime_checkable import is duplicated after the if TYPE_CHECKING block. While not incorrect, it's unconventional.

Suggestion: Move protocol imports to the main import section:

from typing import TYPE_CHECKING, Protocol, runtime_checkable

2. Test Fixture Dependency (Info)

Location: test_secret_resolver_vault_integration.py:80-96

The cleanup_secret fixture depends on vault_handler from conftest but doesn't show the fixture definition. Ensure this fixture is properly defined in tests/integration/handlers/conftest.py.

3. Metrics Collector Thread Safety (Documentation)

Location: secret_resolver.py:167

The protocol docstring mentions "Implementations MUST be thread-safe" but doesn't explain why. Consider adding:

Thread Safety:
    Implementations MUST be thread-safe because SecretResolver's 
    _record_resolution_success and _record_resolution_failure methods
    may be called concurrently from multiple threads during parallel 
    secret resolution operations.

🎯 Performance Considerations

  1. Caching: The implementation properly caches Vault secrets, reducing repeated API calls
  2. Async Support: Vault operations use true async I/O (not thread pools)
  3. Parallel Resolution: get_secrets_async uses asyncio.gather for concurrent fetches ✅
  4. Metrics Overhead: Minimal - uses simple counters and bounded latency samples

🔐 Security Review

✅ PASSED - No security concerns identified:

  • Vault paths never logged
  • Error messages sanitized
  • Correlation IDs for tracing without exposing secrets
  • Path traversal protection maintained
  • Bootstrap secret isolation preserved
  • Metrics failures don't compromise security

✅ ONEX Compliance

✅ Follows all CLAUDE.md requirements:

  • No Any types (uses object for envelopes)
  • Container-based dependency injection
  • Protocol-based metrics collector (duck typing)
  • Error handling with ModelInfraErrorContext.with_correlation()
  • Proper correlation ID propagation
  • Strong typing throughout
  • Comprehensive test coverage

🚀 Recommendation

APPROVE with minor suggestions

This is high-quality work that completes the Vault integration with excellent test coverage, proper error handling, and strong observability features. The only concerns are minor performance optimizations (deque for latency samples) and documentation clarity (sync method limitations).

The implementation follows ONEX patterns closely and maintains security best practices throughout.

📋 Follow-up Items (Optional)

  1. Consider switching _resolution_latencies to collections.deque for O(1) rotation
  2. Add warning in get_secret() docstring about async context limitation
  3. Verify edge case: empty path with just mount point ("secret#field")

Great work on completing this integration! 🎉

- Switch _resolution_latencies from list to deque(maxlen=1000) for O(1) rotation
- Add Warning section to get_secret() docstring about async context limitation
- Consolidate Protocol/runtime_checkable imports into main typing import
- Enhance ProtocolSecretResolverMetrics thread safety docs with reasoning
- Document path parsing edge case for unusual "secret#field" format
@claude

claude Bot commented Jan 17, 2026

Copy link
Copy Markdown

PR Review: Vault Integration for SecretResolver [OMN-1374]

Summary

This PR implements full Vault integration for the SecretResolver, including correlation ID propagation, metrics/observability, and comprehensive test coverage. The implementation is well-structured and follows ONEX patterns closely.


✅ Strengths

1. Strong Type Safety

  • Excellent adherence to the "no Any types" rule throughout
  • Proper use of object for generic payloads where needed
  • Clean type annotations using PEP 604 unions (X | None)

2. Security Best Practices

  • Path traversal protection in _read_file_secret() (lines 1181-1196)
  • Proper secret sanitization in error messages
  • Bootstrap secret isolation to prevent circular dependencies (lines 873-954)
  • TOCTOU race mitigation via atomic file reads (lines 1198-1212)
  • Comprehensive security documentation in module docstring

3. Thread Safety Implementation

  • Excellent two-level locking strategy:
    • threading.Lock for cache access
    • Per-key asyncio.Lock for async resolution deduplication
  • Well-documented edge cases and race conditions (lines 284-308)
  • Check-before-write pattern to handle sync/async races (lines 1038-1040, 1093-1095)

4. Observability & Metrics

  • Comprehensive metrics tracking via ProtocolSecretResolverMetrics protocol
  • Correlation ID propagation throughout the resolution chain
  • Detailed structured logging with sanitized output
  • Built-in metrics (get_resolution_metrics()) plus external collector support

5. Test Coverage

  • Comprehensive unit tests with mocked Vault handler
  • Real integration tests with graceful CI degradation
  • Test isolation via unique secret paths
  • Good coverage of error conditions (auth failures, timeouts, circuit breaker)

🔴 Critical Issues

1. Missing Error Context Validation (src/omnibase_infra/runtime/secret_resolver.py:424-434)

Severity: HIGH

The error context creation doesn't validate the correlation_id before usage:

# Line 424-434
context = ModelInfraErrorContext.with_correlation(
    correlation_id=effective_correlation_id,
    transport_type=EnumInfraTransportType.RUNTIME,
    operation="get_secret",
    target_name="secret_resolver",
)

Problem: If correlation_id parameter is explicitly passed as None, effective_correlation_id would be None (since None or uuid4() evaluates to uuid4() only when the left side is falsy). However, line 407 correctly handles this: effective_correlation_id = correlation_id or uuid4().

Wait - Re-analysis: Actually, this is correct on second look. The pattern correlation_id or uuid4() properly handles None by generating a new UUID. This is NOT an issue.

2. Potential Denial of Service via Unbounded Lock Dictionary (src/omnibase_infra/runtime/secret_resolver.py:352)

Severity: MEDIUM

# Line 352
self._async_key_locks: dict[str, asyncio.Lock] = {}

Problem: The _async_key_locks dictionary grows unbounded. If an application resolves many unique secret names over its lifetime, this could lead to memory exhaustion.

Recommendation:

  • Implement an LRU eviction policy for unused locks
  • Or document the expected max secret count and memory implications
  • Consider using WeakValueDictionary if locks can be garbage collected when not in use

3. Missing Vault Handler Initialization Validation (src/omnibase_infra/runtime/secret_resolver.py:326-341)

Severity: MEDIUM

The constructor accepts vault_handler: HandlerVault | None but doesn't validate that it's properly initialized:

def __init__(
    self,
    config: ModelSecretResolverConfig,
    vault_handler: HandlerVault | None = None,
    metrics_collector: ProtocolSecretResolverMetrics | None = None,
) -> None:
    self._vault_handler = vault_handler  # No validation\!

Problem: If a user passes an uninitialized HandlerVault (before calling .initialize()), resolution will fail with cryptic errors rather than clear configuration errors at startup.

Recommendation:

  • Add a protocol check or explicit validation that vault_handler is initialized
  • Or document the requirement clearly in the docstring (partially done, but not explicit about initialization state)

⚠️ Design Concerns

4. Synchronous Vault Resolution Creates Event Loop (src/omnibase_infra/runtime/secret_resolver.py:1309-1311)

Severity: MEDIUM

# Line 1309-1311
return asyncio.run(
    self._read_vault_secret_async(path, logical_name, effective_correlation_id)
)

Problem: Calling asyncio.run() creates a new event loop for every sync Vault secret resolution. This is expensive and prevents integration with existing async code.

Why this matters:

  • Event loop creation is costly (overhead for each secret)
  • Cannot be used from within async contexts (correctly raises error at line 1298-1303)
  • Forces users to choose between sync and async APIs at design time

Recommendation:

  • Document this limitation more prominently in the public API (get_secret docstring)
  • Consider adding a warning log when sync Vault resolution occurs (to help users identify optimization opportunities)
  • The current implementation is acceptable given the explicit error for async contexts, but this should be highlighted in docs

5. Vault Path Parsing Edge Case (src/omnibase_infra/runtime/secret_resolver.py:1502-1508)

Severity: LOW

# Line 1502-1508
if len(parts) == 1:
    # Edge case: No slash in path - entire path is treated as mount_point
    # with empty vault_path. This format (e.g., "secret#field") is unusual
    # and may not be supported by Vault's KV v2 engine which expects
    # paths like "mount/path". This case is preserved for backwards
    # compatibility but callers should use full paths like "secret/data#field".
    return parts[0], "", field

Problem: This edge case ("secret#field") is documented as "unusual" and "may not be supported by Vault's KV v2 engine", yet the code silently accepts it. This could lead to confusing runtime errors.

Recommendation:

  • Either reject this format explicitly with a clear error
  • Or add a warning log when this edge case is encountered
  • Current "backwards compatibility" justification is weak since this is a new feature (OMN-1374)

🟡 Code Quality Issues

6. Magic Number for Deque Maxlen (src/omnibase_infra/runtime/secret_resolver.py:356)

# Line 356
self._resolution_latencies: deque[tuple[str, float]] = deque(maxlen=1000)

Issue: Hard-coded magic number 1000 for latency sample size.

Recommendation: Extract to a module-level constant:

MAX_LATENCY_SAMPLES = 1000  # Number of latency samples to retain for metrics

7. Inconsistent Error Logging Levels (src/omnibase_infra/runtime/secret_resolver.py:1369-1376, 1380-1388, 1410-1418)

Multiple logger.warning() calls for expected conditions (non-existent secrets, missing fields):

# Line 1369 - Unexpected response format
logger.warning("Unexpected Vault response format...")

# Line 1380 - Non-success status (could be 404 Not Found)
logger.debug("Vault returned non-success status...")

# Line 1410 - Field not found (expected for optional secrets)
logger.debug("Field not found in Vault secret...")

Issue: Line 1369 uses warning while similar conditions use debug. Missing fields (line 1410) is correctly debug, but unexpected response format (line 1369) should also be debug if it's a recoverable "secret not found" scenario.

Recommendation: Standardize logging levels:

  • Use debug for "secret not found" scenarios (expected during resolution)
  • Use warning for unexpected protocol violations or handler errors

8. Metrics Collector Exception Swallowing (src/omnibase_infra/runtime/secret_resolver.py:734-743)

# Line 734-743
except Exception as e:
    # Never let metrics failures affect resolution
    logger.debug(
        "Metrics collector error (ignored): %s",
        e,
        extra={...},
    )

Issue: Broad Exception catch silently ignores all metrics failures. While the comment justifies this ("never let metrics failures affect resolution"), there's no way for operators to detect broken metrics collectors.

Recommendation:

  • Log at warning level (not debug) so operators can see metrics collector failures
  • Or expose a counter for "metrics_collector_errors" in get_resolution_metrics()

📚 Documentation & Testing

9. Excellent Documentation ✅

  • Comprehensive module docstring covering security, thread safety, and Vault integration
  • Inline comments explain complex logic (TOCTOU mitigation, path traversal)
  • Good use of Warning: sections in docstrings (line 387-393)

10. Test Coverage Analysis ✅

Unit tests (tests/unit/runtime/test_secret_resolver.py):

  • Mocked Vault handler integration
  • Correlation ID propagation
  • Metrics tracking
  • Error handling

Integration tests (tests/integration/runtime/test_secret_resolver_vault_integration.py):

  • Real Vault integration with graceful CI skip
  • Test isolation via unique secret paths
  • Cleanup fixtures

Gaps:


🔍 ONEX Compliance Check

✅ Compliant:

  • Strong typing: No Any types, proper use of object for generics
  • PEP 604 unions: X | None throughout
  • Error handling: Proper use of infrastructure errors (InfraAuthenticationError, InfraTimeoutError, InfraUnavailableError)
  • Correlation ID: Proper propagation via ModelInfraErrorContext.with_correlation()
  • Security patterns: Secret sanitization, path traversal protection, bootstrap secret isolation

⚠️ Minor Deviations:

  • No backwards compatibility: ✅ Correctly not maintaining backwards compatibility (per CLAUDE.md)
  • Metrics protocol: Good use of runtime-checkable protocol for duck typing

🎯 Recommendations Summary

Must Fix (Before Merge):

  1. Add bounded eviction for _async_key_locks or document memory implications (Issue Add Claude Code GitHub Workflow #2)
  2. Validate or document Vault handler initialization requirement (Issue feat: RedPanda Event Bus Integration with Fail-Fast Infrastructure #3)
  3. Reject or warn on "secret#field" Vault path format (Issue feat: Complete Phase 2 infrastructure migration to ONEX nodes #5)

Should Fix (Post-Merge):

  1. Document sync Vault resolution event loop cost more prominently (Issue feat: Complete infrastructure containers operational with Docker secrets #4)
  2. Standardize logging levels for Vault response handling (Issue feat: Complete Hook Node Protocol Integration and Production Readiness #7)
  3. Elevate metrics collector errors to warning level (Issue feat: migrate 96 models to domain-organized structure #8)
  4. Extract MAX_LATENCY_SAMPLES = 1000 constant (Issue feat: Complete Hook Node Protocol Integration and Production Readiness #6)

Nice to Have:

  1. Add tests for unbounded lock growth scenario
  2. Add tests for uninitialized Vault handler
  3. Add tests for Vault path edge cases

🏆 Final Verdict

Approval Status: APPROVE with Minor Changes Required

This is a high-quality implementation that demonstrates strong adherence to ONEX patterns, excellent security practices, and comprehensive observability. The identified issues are mostly documentation/hardening concerns rather than correctness bugs.

Blocking Items (fix before merge):

Non-Blocking Items (can be addressed in follow-up):

Great work on:

  • Thread safety implementation and documentation
  • Security hardening (path traversal, TOCTOU, secret sanitization)
  • Metrics/observability integration
  • Test isolation and graceful CI degradation

Estimated Impact: This PR adds 1,609 lines with strong test coverage. The Vault integration is well-architected and sets a good precedent for future infrastructure integrations.

cc: @jonahgabriel

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/omnibase_infra/runtime/secret_resolver.py`:
- Around line 777-783: The current _record_resolution_failure unconditionally
calls self._metrics_collector.record_cache_miss on any failure; change it to
only call record_cache_miss for actual cache-miss conditions. Modify
_record_resolution_failure (and callers if needed) to accept or inspect the
failure/exception (e.g., an error object or failure_type) and only call
self._metrics_collector.record_cache_miss() when the failure indicates a true
cache miss (for example SecretNotFound/CacheMissError or a specific enum value),
but continue to call record_resolution_failure(source_type) for all failures;
leave auth/timeouts/unavailable errors without the cache-miss call. Ensure you
reference self._metrics_collector.record_resolution_failure and
self._metrics_collector.record_cache_miss in the conditional logic.
🧹 Nitpick comments (6)
src/omnibase_infra/runtime/secret_resolver.py (2)

682-691: set_metrics_collector is not thread-safe.

This method directly assigns to _metrics_collector without holding _lock. If called while _record_resolution_success or _record_resolution_failure is executing, there could be a race condition where the collector reference changes mid-operation.

♻️ Suggested fix for thread-safe assignment
     def set_metrics_collector(
         self, collector: ProtocolSecretResolverMetrics | None
     ) -> None:
         """Set the external metrics collector.

         Args:
             collector: Metrics collector implementing ProtocolSecretResolverMetrics,
                 or None to disable external metrics collection.
         """
-        self._metrics_collector = collector
+        with self._lock:
+            self._metrics_collector = collector

1361-1427: Vault response handling converts all secret values to strings, regardless of stored type.

Vault KV v2 can store non-string values (numbers, booleans, objects, arrays) via the HTTP API, but the code on line 1427 converts all values to strings using str(value). This means:

  • Non-string values will be stringified (e.g., {"key": "val"} becomes '{"key": "val"}')
  • Complex types lose their semantic meaning

Confirm whether string-only secret values are the intended design, or whether non-string values should be preserved or explicitly rejected with a clear error.

tests/unit/runtime/test_secret_resolver.py (2)

1294-1334: Unused import of ModelHandlerOutput.

Line 1298 imports ModelHandlerOutput from omnibase_core.models.dispatch, but it's never used in the test. The mock returns a MagicMock with a result attribute instead.

♻️ Remove unused import
     def test_vault_resolves_secret_with_mocked_handler(self) -> None:
         """Vault secrets should resolve when handler returns valid response."""
         from unittest.mock import AsyncMock, MagicMock

-        from omnibase_core.models.dispatch import ModelHandlerOutput
-
         # Create a mock vault handler
         mock_vault_handler = MagicMock()

1729-1750: Redundant import of uuid4 inside test method.

Line 1733 imports uuid4 inside test_vault_auth_error_propagates, but uuid4 is already imported at the module level (line 34). The import inside the function is unused and the uuid.uuid4() call isn't actually made in this test.

♻️ Remove redundant import
     `@pytest.mark.asyncio`
     async def test_vault_auth_error_propagates(self) -> None:
         """Should propagate InfraAuthenticationError from handler."""
         from unittest.mock import AsyncMock, MagicMock
-        from uuid import uuid4

         from omnibase_infra.enums import EnumInfraTransportType
         from omnibase_infra.errors import (
             ModelInfraErrorContext,
         )
tests/integration/runtime/test_secret_resolver_vault_integration.py (2)

79-96: Cleanup fixture should be more defensive about delete operation errors.

The cleanup fixture catches all exceptions but only logs at debug level. If the delete operation fails for reasons other than "secret doesn't exist" (e.g., permission issues), this could leave test artifacts in Vault without clear indication.

♻️ Consider differentiating error types in cleanup
     # Cleanup: delete the test secret (idempotent)
     try:
         envelope = {
             "operation": "vault.delete_secret",
             "payload": {"path": unique_secret_path, "mount_point": "secret"},
             "correlation_id": str(uuid.uuid4()),
         }
         await vault_handler.execute(envelope)
     except Exception as e:
-        logger.debug("Cleanup: secret may not exist - %s", e)
+        # Log at warning level for unexpected errors (not just "not found")
+        logger.warning(
+            "Cleanup: failed to delete test secret %s - %s",
+            unique_secret_path,
+            e,
+        )

352-394: Graceful degradation tests ensure reliability without Vault.

The TestSecretResolverVaultGracefulDegradation class verifies the system behaves correctly when vault_handler is None:

  • Returns None instead of raising
  • Tracks failure metrics

These tests don't require Vault connectivity since they test the handler-absent path, but they're still guarded by the module skip markers. Consider whether these should run unconditionally.

♻️ Consider removing skip markers for tests that don't need Vault

The graceful degradation tests don't actually interact with Vault since they test the vault_handler=None path. You could mark them to run unconditionally:

 class TestSecretResolverVaultGracefulDegradation:
     """Tests for graceful degradation scenarios."""

+    # Note: These tests run even without Vault since they test handler-absent behavior
+    pytestmark = []  # Override module-level skip markers
+
     def test_vault_handler_none_returns_none(self) -> None:

Alternatively, consider moving these tests to the unit test file since they don't require Vault infrastructure.

Comment thread src/omnibase_infra/runtime/secret_resolver.py Outdated
…-1374]

- Fix record_cache_miss called unconditionally in _record_resolution_failure
- Add thread safety to set_metrics_collector with lock
- Make cleanup fixture more defensive with specific exception handling
- Fix inconsistent logging levels (security=ERROR, config=WARNING, expected=DEBUG)
- Add comprehensive documentation for Vault string conversion behavior
- Remove unused imports and organize imports properly
@claude

claude Bot commented Jan 17, 2026

Copy link
Copy Markdown

PR Review: Vault Integration for SecretResolver [OMN-1374]

This PR implements comprehensive Vault integration for SecretResolver with correlation ID propagation and observability metrics.

Strengths

  1. Excellent Security Posture - No secret leakage in logs, proper error sanitization, path traversal protection
  2. Robust Thread Safety - Two-level locking strategy with threading.Lock and per-key asyncio.Lock
  3. Type Safety & ONEX Compliance - Zero Any types, PEP 604 unions, strong typing throughout
  4. Comprehensive Error Handling - Proper error hierarchy with context propagation
  5. Observability & Metrics - Built-in tracking with external collector support

Critical Issue: asyncio.Lock Event Loop Binding

Location: secret_resolver.py:1581

asyncio.Lock objects are bound to the event loop in which they are created. If _get_async_key_lock() is called from different event loops (sequential asyncio.run() calls), the lock may raise RuntimeError.

Recommendation: Track event loop with each lock and recreate if loop changes, OR document that SecretResolver instances must not be shared across asyncio.run() boundaries.

Impact: Medium - Will fail in CLI tools or tests that call asyncio.run() multiple times.

Medium Issue: Missing Cache Miss Metrics

Location: secret_resolver.py:795-797

While self._misses is incremented, the external metrics collector record_cache_miss() is NEVER called. External monitoring systems won't see cache miss metrics.

Recommendation: Add record_cache_miss() calls where self._misses += 1 occurs.

Minor Issues

  1. Vault path edge case (path=secret#field without slash) needs validation
  2. ProtocolSecretResolverMetrics docstring incomplete

Test Coverage

Well covered: Vault path parsing, error handling, correlation ID, integration tests
Missing: Event loop binding tests, metrics collector error handling, path edge cases

Recommendations

Must Fix: asyncio.Lock event loop binding issue
Should Fix: Add record_cache_miss() calls, add Vault path validation
Nice to Have: Complete docstring, add missing test coverage

Final Verdict

APPROVE with required fixes

High-quality code with excellent security and ONEX compliance. The asyncio.Lock issue must be addressed before merge.

Risk: Medium (due to asyncio.Lock) to Low (once fixed)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

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/secret_resolver.py (1)

361-361: Prevent self-deadlock when recording metrics under _lock.
get_secret/get_secret_async call _record_resolution_success while holding _lock, and _record_resolution_success re-acquires _lock, which will deadlock in sync paths.

🐛 Proposed fix (use a re-entrant lock)
-        self._lock = threading.Lock()
+        self._lock = threading.RLock()

Also applies to: 421-427, 515-519, 726-731

🧹 Nitpick comments (2)
src/omnibase_infra/runtime/secret_resolver.py (2)

338-343: Consider container-based DI for the new constructor dependency.
With the new metrics_collector parameter, it’s a good moment to align SecretResolver with the ModelONEXContainer pattern for service construction. As per coding guidelines, use the container in __init__ for service dependencies.


1393-1401: Use JsonType for Vault envelopes to match typing conventions.
The envelope is JSON-compatible; using JsonType will align with internal type standards and avoid object leakage in API boundaries. As per coding guidelines, prefer JsonType for JSON payloads.

…r [OMN-1374]

- Fix deadlock: Change threading.Lock to threading.RLock (reentrant)
  - get_secret() holds lock while calling _record_resolution_success()
  - _record_resolution_success() also needs the lock
  - Non-reentrant Lock caused deadlock; RLock allows same thread to re-acquire

- Fix thread safety race in metrics collector access
  - Capture _metrics_collector reference while holding lock
  - Use captured reference outside lock to avoid race with set_metrics_collector()

- Use JsonType for Vault envelope type annotation (ONEX convention)

- Document _async_key_locks unbounded growth limitation and DoS mitigation

- Update all docstrings: threading.Lock -> threading.RLock
@claude

claude Bot commented Jan 17, 2026 •

Copy link
Copy Markdown

Code Review - PR #167: Vault Integration for SecretResolver

Summary

This PR implements complete Vault integration for the SecretResolver with correlation ID propagation and metrics tracking. The implementation is comprehensive and well-tested. Overall quality is excellent with only minor suggestions for improvement.


✅ Strengths

1. Excellent Documentation

  • Comprehensive module docstring with security considerations, memory handling, and Vault integration details
  • Clear explanation of type conversion for Vault values (str(), not JSON)
  • Thread safety documentation is thorough and accurate
  • Security considerations are well-documented (path traversal, information disclosure, etc.)

2. Strong Type Safety

  • Zero Any types - fully compliant with CLAUDE.md zero-tolerance policy ✅
  • Proper use of JsonType for envelope data
  • TYPE_CHECKING guard for HandlerVault import to avoid circular dependencies
  • All return types are explicit and correct

3. Comprehensive Test Coverage

  • Unit tests: 15 test classes covering basic resolution, caching, thread safety, async, Vault mocking, correlation ID, metrics
  • Integration tests: Real Vault integration with proper test isolation and graceful skipping
  • Thread safety tests are excellent - test concurrent reads, refreshes, and sync/async race conditions
  • Edge case coverage is thorough (path traversal, missing fields, empty responses)

4. Security Best Practices

  • Path traversal protection in _read_file_secret() (lines 1252-1267)
  • Sanitized error messages - never log Vault paths or secret values
  • Bootstrap secret isolation prevents circular dependencies
  • Correlation ID propagation for distributed tracing

5. Error Handling

  • Proper error context with ModelInfraErrorContext.with_correlation()
  • Infrastructure errors are re-raised with proper context
  • Graceful degradation when vault_handler is None

🔍 Code Quality Issues

1. Potential Race Condition in Async Resolution (Minor)

Location: secret_resolver.py:1164-1166

# THREAD SAFETY: Use lock for cache write to prevent race with sync callers
# Check-before-write pattern avoids unnecessary overwrites if sync caller
# already cached this secret between our cache check and now
with self._lock:
    if logical_name not in self._cache:
        self._cache_secret(logical_name, secret, source_type)

Issue: The check-before-write pattern is good, but there's still a TOCTOU (time-of-check-time-of-use) window:

  • Thread A checks logical_name not in self._cache → True
  • Thread B acquires lock, writes cache
  • Thread A acquires lock, writes cache again (redundant)

Recommendation: This is acceptable as documented ("last-write-wins"), but consider using setdefault for atomic check-and-set:

with self._lock:
    if logical_name not in self._cache:
        self._cache_secret(logical_name, secret, source_type)
    # OR: self._cache.setdefault(logical_name, cached_entry)

Severity: Low - documented behavior, no correctness issue


2. Unbounded Dictionary Growth (Documented Limitation)

Location: secret_resolver.py:578-599

The _async_key_locks dictionary grows without bounds. This is acknowledged and documented, but worth highlighting:

# Known Limitation - Unbounded Dictionary Growth:
#     The _async_key_locks dictionary grows monotonically...

Risk: DoS if attacker controls logical names passed to get_secret_async().

Mitigation: Already documented - "validate logical names against configured mappings before calling resolution methods". Consider adding a size limit check:

if len(self._async_key_locks) > 10000:  # Sanity limit
    logger.warning("Async key locks dictionary exceeds 10k entries - potential DoS")

Severity: Low - well-documented, bounded in practice


3. Inconsistent Metrics Collector Error Handling (Minor)

Location: secret_resolver.py:779-788, 840-848

Metrics collector errors are silently suppressed with debug logging. This is correct for metrics (never fail resolution due to metrics), but consider:

except Exception as e:
    # Never let metrics failures affect resolution
    logger.debug(
        "Metrics collector error (ignored): %s",
        e,
        # SUGGESTION: Add exception type for better diagnostics
        extra={
            "logical_name": logical_name,
            "correlation_id": str(correlation_id),
            "error_type": type(e).__name__,  # ADD THIS
        },
    )

Severity: Very Low - quality-of-life improvement


🚀 Performance Considerations

1. Vault Path Parsing on Every Call

Location: secret_resolver.py:1432

mount_point, vault_path, field = self._parse_vault_path_components(path)

Optimization: Path parsing happens on every Vault resolution, even for cached secrets. This is mitigated by cache hits, but consider pre-parsing paths during __init__ for explicit mappings.

Impact: Negligible - string split operations are O(n) where n is path length (~10-50 chars)


2. Sequential Secret Resolution in Sync get_secrets()

Location: secret_resolver.py:478-483

def get_secrets(self, logical_names: list[str], ...):
    return {
        name: self.get_secret(name, required=required, correlation_id=correlation_id)
        for name in logical_names
    }

Note: Already documented - "This sync method resolves secrets sequentially. For better latency... prefer get_secrets_async()"

Recommendation: Perfect as-is. Documentation is clear.


🛡️ Security Review

1. Path Traversal Protection ✅

  • Proper use of Path.resolve() and is_relative_to()
  • Distinguishes between relative (validated) and absolute (trusted) paths
  • Error logging at appropriate levels (ERROR for traversal attempts)

2. Information Disclosure Protection ✅

  • File paths never logged in errors
  • Vault paths never logged (could reveal secret structure)
  • Secret values wrapped in SecretStr
  • Introspection methods use _mask_source_path()

3. Memory Handling (Acknowledged Limitation)

Raw secret values are briefly held in local variables before wrapping in SecretStr. This is documented and acceptable for most use cases:

"Raw secret values (plain strings) are briefly held in local variables during resolution before being wrapped in SecretStr. Python's garbage collector will reclaim this memory, but there is no explicit secure memory wiping."

Recommendation: Consider mentioning mlock() or secure memory libraries for high-security deployments in the docstring (already mentions "dedicated secret management libraries").


📊 Test Coverage Analysis

Unit Tests (test_secret_resolver.py)

  • ✅ Basic resolution (env, file, convention fallback)
  • ✅ Required vs optional secrets
  • ✅ Caching (hit/miss, TTL, expiration, refresh)
  • ✅ Thread safety (concurrent reads, refresh races)
  • ✅ Async API (resolution, cache, error handling)
  • ✅ Vault integration (mocked HandlerVault)
  • ✅ Correlation ID propagation
  • ✅ Metrics tracking (success/failure counts, latency, external collector)
  • ✅ Bootstrap secrets (env-only resolution)
  • ✅ Security (path traversal, file size limits)

Integration Tests (test_secret_resolver_vault_integration.py)

  • ✅ Real Vault secret resolution
  • ✅ Field-specific and first-value extraction
  • ✅ Correlation ID propagation through Vault calls
  • ✅ Error handling (not found, auth failures, timeouts)
  • ✅ Caching with real Vault latency
  • ✅ Graceful skipping when Vault unavailable

Coverage: Excellent. Edge cases are well-tested.


📋 CLAUDE.md Compliance

Policy Status Notes
Zero Any types ✅ PASS No Any types detected
PEP 604 unions ✅ PASS Uses `X
Pydantic models ✅ PASS All data structures are proper models
Error handling ✅ PASS Uses ModelInfraErrorContext.with_correlation()
Correlation ID propagation ✅ PASS Implemented throughout
Security patterns ✅ PASS Path traversal protection, sanitized errors
No backwards compatibility ✅ PASS Breaking changes are acceptable per CLAUDE.md

🎯 Recommendations

Priority: Low

  1. Add error_type to metrics collector exception logging (lines 779-788, 840-848)
  2. Consider size limit warning for _async_key_locks dictionary (line 607)
  3. Document mlock() alternative for high-security deployments (module docstring)

Priority: Very Low (Optional)

  1. Pre-parse Vault paths during __init__ for slight performance gain (negligible impact)

✅ Approval Status

APPROVED - This PR is production-ready with excellent code quality, comprehensive tests, and strong adherence to ONEX patterns.

Final Score: 9.5/10

  • Code Quality: Excellent
  • Test Coverage: Excellent
  • Documentation: Excellent
  • Security: Excellent
  • Performance: Good (intentional trade-offs for clarity)

Suggested merge after:

  • ✅ Pre-commit hooks pass
  • ✅ CI validators pass (any_types, mypy, ruff)
  • ✅ Integration tests pass (or skip gracefully without Vault)

Great work! 🎉

…est [OMN-1374]

The test was accessing correlation_id via context dict attribute
(exc_info.value.context.correlation_id) but ModelOnexError exposes
correlation_id as a top-level property. Fixed to use the correct
access pattern (exc_info.value.correlation_id).
…pede [OMN-1374]

Add 0-10% random jitter to cache TTL values in SecretResolver to prevent
cache stampede (thundering herd) scenarios where many cached entries
expire simultaneously, causing burst resolution requests.

Changes:
- Add `import random` for jitter calculation
- Modify `_cache_secret()` to apply jitter factor to base TTL
- Update docstring to document jitter behavior

This addresses the PR #167 review feedback requesting TTL jitter support.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/omnibase_infra/runtime/secret_resolver.py (2)

426-455: Cache misses aren’t recorded, so hit/miss metrics are misleading.

_misses is only incremented when resolution returns None, so successful cache misses are never counted, and external collectors never receive record_cache_miss() (it’s not called anywhere). That skews cache stats and the metrics payload. Consider recording the miss immediately after a cache lookup fails (once per request) and remove the increment on the final None path to avoid double-counting.

🐛 Proposed fix: record cache miss on lookup
@@
         with self._lock:
             # Check cache first
             cached = self._get_from_cache(logical_name)
             if cached is not None:
                 self._record_resolution_success(
                     logical_name, "cache", effective_correlation_id
                 )
                 return cached
+            # Cache miss accounting (before resolution)
+            self._misses += 1
+            collector = self._metrics_collector
+            if collector is not None and hasattr(collector, "record_cache_miss"):
+                try:
+                    collector.record_cache_miss()
+                except Exception as e:
+                    logger.debug(
+                        "Metrics collector error (ignored): %s",
+                        e,
+                        extra={
+                            "logical_name": logical_name,
+                            "correlation_id": str(effective_correlation_id),
+                        },
+                    )
 
             # Resolve from source
             result = self._resolve_secret(logical_name, effective_correlation_id)
 
             if result is None:
-                self._misses += 1
                 if required:
                     context = ModelInfraErrorContext.with_correlation(
                         correlation_id=effective_correlation_id,
                         transport_type=EnumInfraTransportType.RUNTIME,
@@
-        # Use threading lock for cache check (fast operation, prevents race with sync)
-        with self._lock:
+        collector = None
+        # Use threading lock for cache check (fast operation, prevents race with sync)
+        with self._lock:
             cached = self._get_from_cache(logical_name)
             if cached is not None:
                 self._record_resolution_success(
                     logical_name, "cache", effective_correlation_id
                 )
                 return cached
+            self._misses += 1
+            collector = self._metrics_collector
+
+        if collector is not None and hasattr(collector, "record_cache_miss"):
+            try:
+                collector.record_cache_miss()
+            except Exception as e:
+                logger.debug(
+                    "Metrics collector error (ignored): %s",
+                    e,
+                    extra={
+                        "logical_name": logical_name,
+                        "correlation_id": str(effective_correlation_id),
+                    },
+                )
@@
             if result is None:
-                with self._lock:
-                    self._misses += 1
                 if required:
                     context = ModelInfraErrorContext.with_correlation(
                         correlation_id=effective_correlation_id,
                         transport_type=EnumInfraTransportType.RUNTIME,

Also applies to: 518-566


1036-1060: Record failure metrics when Vault operations raise exceptions.

_read_vault_secret_async raises InfraAuthenticationError, InfraTimeoutError, InfraUnavailableError, and SecretResolutionError, but these exceptions propagate unhandled through _resolve_secret and _resolve_secret_async. When exceptions occur, failure metrics are never recorded because the _record_resolution_failure call at line 1059 (sync) and line 1149 (async) is only reached when value is None, not when exceptions are raised.

Wrap both the sync call at line 1053 and async call at line 1143 with try-catch blocks that record _record_resolution_failure before re-raising, matching the pattern already used for the handler_not_configured case.

🧹 Nitpick comments (1)
src/omnibase_infra/runtime/secret_resolver.py (1)

695-719: Use JsonType (or a model) for metrics payload typing.

dict[str, object] is too loose for a JSON payload and doesn’t align with the JsonType guideline. Consider dict[str, JsonType] (or a dedicated ModelSecretResolverMetrics Pydantic model if you want stronger structure).

♻️ Suggested typing tweak
-    def get_resolution_metrics(self) -> dict[str, object]:
+    def get_resolution_metrics(self) -> dict[str, JsonType]:

As per coding guidelines, use JsonType for JSON-compatible values.

@jonahgabriel

Copy link
Copy Markdown
Collaborator Author

@claude review this

@claude

claude Bot commented Jan 18, 2026 •

Copy link
Copy Markdown

Claude finished @jonahgabriel's task in 2m 25s —— View job


Code Review: Vault Integration for SecretResolver [OMN-1374]

Overview

This PR implements comprehensive Vault integration for SecretResolver with correlation ID propagation and observability metrics. The implementation is well-structured and production-ready with excellent test coverage and strong adherence to ONEX patterns.

Verdict: APPROVE ✅


Strengths

1. Excellent Type Safety ✅

  • Zero Any types - fully compliant with CLAUDE.md zero-tolerance policy
  • Proper use of JsonType for Vault envelopes (line 1435)
  • Strong typing throughout with PEP 604 unions (X | None)

2. Comprehensive Documentation 📚

  • Module docstring covers security, thread safety, Vault integration (lines 3-121)
  • Clear path format documentation (mount/path#field)
  • Type handling explained for non-string values (lines 85-92)
  • Thread safety patterns well-documented (lines 277-322)

3. Robust Error Handling 🛡️

  • Proper error hierarchy: infrastructure errors re-raised with context (lines 1525-1533)
  • Generic errors wrapped in SecretResolutionError (lines 1534-1546)
  • Correlation ID propagation through error context (lines 1536-1541)
  • Graceful degradation when vault_handler is None (lines 1041-1056, 1134-1149)

4. Strong Security Practices 🔒

  • Path traversal protection for file-based secrets (lines 1252-1267)
  • Secret sanitization: Vault paths never logged (line 55)
  • Bootstrap secret isolation prevents circular dependencies (lines 1015-1025, 1100-1115)
  • TOCTOU mitigation via atomic file reads (lines 1269-1283)
  • Error messages never expose file paths or secret values (lines 1286-1321)

5. Excellent Test Coverage 🧪

  • Unit tests: 15 test classes, 2000+ lines covering basic resolution, caching, thread safety, async, Vault mocking, correlation ID, metrics
  • Integration tests: Real Vault integration with proper test isolation and graceful CI skipping (467 lines)
  • Thread safety tests verify concurrent reads, refreshes, and sync/async race conditions
  • Edge cases well-covered: path traversal, symlink loops, missing fields

6. Observability & Metrics 📊

  • Built-in metrics tracking: latency samples (deque with maxlen=1000), success/failure counts by source type
  • External metrics collector support via ProtocolSecretResolverMetrics protocol (lines 166-258)
  • Structured logging with correlation IDs (lines 791-801)
  • Metrics failures never affect resolution (fail-safe design, lines 779-788)

Issues Addressed from CodeRabbit Reviews

✅ Fixed Issues

  1. RLock for metrics recording (PR review 2026-01-17 22:21)

    • ✅ FIXED: Changed threading.Lock to threading.RLock (line 366)
    • Prevents self-deadlock when _record_resolution_success is called while holding _lock
  2. Cache miss metrics not recorded (PR review 2026-01-18 11:41)

    • ✅ FIXED: NOT calling record_cache_miss() in _record_resolution_failure (lines 832-838)
    • Correctly distinguishes resolution failures from cache misses
  3. Thread-safe set_metrics_collector (PR review 2026-01-17 20:57)

    • ✅ FIXED: Added with self._lock in set_metrics_collector (lines 730-731)

Remaining Concerns

1. ⚠️ Potential Issue: Unbounded _async_key_locks Dictionary Growth

Location: Line 369, 607-609

Issue: The _async_key_locks dictionary grows unbounded as new logical names are resolved asynchronously. While well-documented (lines 578-599), this could lead to memory exhaustion in long-running processes with dynamic secret names.

Risk: Medium - bounded in practice for typical use cases (<100 secrets), but vulnerable to DoS if attacker controls logical names.

Recommendation:

  • Document mitigation: "Validate logical names against configured mappings before calling resolution methods"
  • Consider adding size limit warning at 1000+ entries (non-blocking log)

Example fix (optional):

# In _get_async_key_lock
if len(self._async_key_locks) > 1000:
    logger.warning(
        "Async key locks dictionary exceeds 1000 entries - potential DoS risk. "
        "Validate logical names against configured mappings.",
        extra={"lock_count": len(self._async_key_locks)}
    )

Fix this →


2. 💡 Minor: Vault Path Edge Case Validation

Location: Lines 1598-1604

Issue: The edge case "secret#field" (mount_point="secret", path="", field="password") is preserved for "backwards compatibility", but this is a new feature (OMN-1374), so there's no backwards compatibility to maintain.

Recommendation: Either reject this format explicitly with a clear error or add a warning log when encountered.

Example fix:

if len(parts) == 1:
    # Reject unusual format that may not be supported by Vault KV v2
    raise SecretResolutionError(
        f"Invalid Vault path format: {path}. Expected 'mount/path#field' format.",
        context=context,
        logical_name=logical_name,
    )

Fix this →


3. 💡 Minor: Magic Number for Latency Samples

Location: Line 373

Issue: Hard-coded maxlen=1000 for latency sample deque.

Recommendation: Extract to module-level constant for clarity:

# After MAX_SECRET_FILE_SIZE
MAX_LATENCY_SAMPLES = 1000  # Number of latency samples to retain for metrics

Impact: Very Low - quality-of-life improvement


4. 📝 Minor: Metrics Collector Error Logging Level

Location: Lines 779-788, 839-848

Issue: Metrics collector errors are logged at debug level, making it hard for operators to detect broken metrics collectors.

Recommendation: Log at warning level instead:

logger.warning(  # Changed from logger.debug
    "Metrics collector error (ignored): %s",
    e,
    extra={...},
)

Impact: Low - operational visibility improvement


CLAUDE.md Compliance

Policy Status Notes
Zero Any types ✅ PASS No Any types detected
PEP 604 unions ✅ PASS Uses `X
Pydantic models ✅ PASS All data structures are proper models
Error handling ✅ PASS Uses ModelInfraErrorContext.with_correlation()
Correlation ID propagation ✅ PASS Implemented throughout (lines 424, 516, 1012, 1098, 1357, 1429)
Security patterns ✅ PASS Path traversal protection, sanitized errors, bootstrap isolation
No backwards compatibility ✅ PASS Breaking changes acceptable per CLAUDE.md
Strong typing ✅ PASS JsonType for envelopes, SecretStr for values
Thread safety ✅ PASS RLock for metrics, per-key async locks, check-before-write pattern

Performance Considerations

✅ Strengths:

  1. Efficient cache rotation: deque(maxlen=1000) provides O(1) rotation (line 373)
  2. Parallel async resolution: get_secrets_async uses asyncio.gather (line 653)
  3. TTL jitter: Prevents cache stampede with 0-10% jitter (lines 1631-1632)

⚠️ Minor Concerns:

  1. Sync Vault resolution creates new event loop: asyncio.run() has overhead (line 1380)
    • Mitigation: Well-documented limitation (lines 404-409), explicit error when called from async context

Security Review

✅ EXCELLENT Security Posture

  1. Path traversal protection: is_relative_to() check for relative paths (lines 1259-1267)
  2. Secret sanitization: File paths never logged, Vault paths never logged (lines 52, 55)
  3. Bootstrap secret isolation: Prevents circular Vault dependency (lines 1015-1025)
  4. TOCTOU mitigation: Atomic file reads instead of stat-then-read (lines 1269-1283)
  5. File size limits: 1MB max to prevent memory exhaustion (lines 1276-1282)
  6. Error message sanitization: Logical names only, never paths or values (lines 1286-1321)

📋 Security Checklist:

  • ✅ No secret leakage in logs
  • ✅ Path traversal blocked
  • ✅ TOCTOU race mitigated
  • ✅ Memory exhaustion prevented (file size limits)
  • ✅ Bootstrap secrets isolated
  • ✅ Error messages sanitized

Test Coverage Analysis

Unit Tests (test_secret_resolver.py): EXCELLENT

  • 15 test classes covering:
    • Basic resolution (env, file, convention fallback)
    • Required vs optional secrets
    • Caching (hit/miss, TTL, expiration, refresh)
    • Thread safety (concurrent reads, refresh races, sync/async coordination)
    • Async API
    • Vault integration (mocked)
    • Correlation ID propagation
    • Metrics tracking
    • Bootstrap secrets
    • Security (path traversal, file size limits, symlink loops)

Integration Tests (test_secret_resolver_vault_integration.py): EXCELLENT

  • Real Vault integration with:
    • Field-specific and first-value extraction
    • Correlation ID propagation through Vault calls
    • Error handling (not found, missing fields)
    • Caching behavior with real Vault latency
    • Metrics tracking
    • Graceful skipping when Vault unavailable (CI-friendly)

Coverage Gaps: MINOR

  • ✅ No significant gaps - edge cases well-covered
  • ✅ Thread safety thoroughly tested
  • ✅ Security scenarios (path traversal, symlink loops) verified

Recommendations

Must Fix: NONE

All critical issues from previous reviews have been addressed.

Should Fix (Post-Merge):

  1. Add size limit warning for _async_key_locks dictionary (Issue feat: PostgreSQL Adapter with Comprehensive Tests and Structured Logging #1)
  2. Validate or reject "secret#field" Vault path format (Issue Add Claude Code GitHub Workflow #2)

Nice to Have:

  1. Extract MAX_LATENCY_SAMPLES = 1000 constant (Issue feat: RedPanda Event Bus Integration with Fail-Fast Infrastructure #3)
  2. Elevate metrics collector errors to warning level (Issue feat: Complete infrastructure containers operational with Docker secrets #4)
  3. Document sync Vault resolution event loop cost more prominently in public API

Final Verdict

APPROVED ✅

This is high-quality, production-ready code that demonstrates:

  • ✅ Strong adherence to ONEX patterns
  • ✅ Excellent security practices
  • ✅ Comprehensive test coverage
  • ✅ Robust error handling
  • ✅ Strong type safety
  • ✅ Well-documented edge cases and design decisions

The identified issues are minor enhancements rather than correctness bugs. The implementation is safe to merge and deploy.

Risk Assessment: LOW

  • No blocking issues
  • Well-tested with real Vault integration
  • Security best practices followed
  • Thread-safe design verified

Great work on this implementation! 🎉


Branch: jonah/omn-1374-secretresolver-complete-vault-integration-and-add

…solver [OMN-1374]

Address remaining review feedback items:

1. DoS mitigation: Add warning when _async_key_locks dictionary reaches
   1000 entries to alert operators of potential memory exhaustion

2. Extract MAX_LATENCY_SAMPLES constant (was magic number 1000) for
   latency deque maxlen

3. Vault path edge case: Log warning when unusual path format detected
   (e.g., "secret#field" without path component)

4. Metrics collector errors: Change logging from debug to warning level
   so operators can detect broken metrics collectors

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/omnibase_infra/runtime/secret_resolver.py`:
- Around line 1622-1627: The warning currently logs a raw Vault path via
logger.warning and the extra={"vault_path": path}, which violates the "no Vault
paths in logs" rule; update the logger.warning call in secret_resolver.py (the
block using logger.warning and the path variable) to emit a generic message that
does not include the path or attach it in extra (e.g., mention the expected
'mount/path#field' format and potential KV v2 issue) and remove any usage of the
path variable from the log and extra fields.

Comment thread src/omnibase_infra/runtime/secret_resolver.py Outdated
Comment thread src/omnibase_infra/runtime/secret_resolver.py
@claude

claude Bot commented Jan 18, 2026

Copy link
Copy Markdown

PR 167 Review: Vault Integration for SecretResolver

Executive Summary

This PR implements Vault integration with strong ONEX pattern adherence. Excellent security awareness and comprehensive testing, but THREE CRITICAL ISSUES must be fixed before merge.

Critical Issues

1. Security: Vault Path Logged (Line 1622-1627)

The raw Vault path is logged in edge case, violating the security policy. This reveals secret structure to attackers. MUST mask the path and remove the extra vault_path parameter.

2. DoS Vector: Unbounded Dictionary Growth (Line 614-616)

The _async_key_locks dictionary grows without bounds. Attackers controlling logical names can exhaust memory. MUST implement LRU cache with configurable max size.

3. Missing Type Conversion Tests

Docstring documents int/bool/dict to string conversion, but NO tests verify this critical behavior. MUST add test cases.

High Priority Issues

  1. Race Condition: Check-before-write pattern (lines 1128-1130, 1184-1185) can cause cache inconsistency
  2. Missing Error Codes: Need VAULT_UNAVAILABLE, SECRET_NOT_FOUND, HANDLER_NOT_CONFIGURED
  3. Test Coverage Gap: Sync Vault resolution inadequately tested

Strengths

  • Excellent security (SecretStr, path traversal protection, bootstrap isolation)
  • Comprehensive testing with edge cases
  • Strong ONEX compliance (PEP 604 unions, error hierarchy)
  • Performance optimization (per-key async locks, cache jitter)

ONEX Compliance: PASS (with security caveats)

Recommendation

Request changes for 3 critical issues, then re-review. High-quality implementation overall. Great work on documentation and security considerations!

…OMN-1374]

- Security: Remove vault path from warning log to prevent info disclosure
- Typing: Add ModelSecretResolverMetrics Pydantic model for get_resolution_metrics()
- Docs: Add thread safety docs to set_metrics_collector() method
- Docs: Add container-based DI note to constructor docstring
- Tests: Update to use model attributes instead of dict key access

All 70 unit tests pass. Type checking clean.
@claude

claude Bot commented Jan 18, 2026

Copy link
Copy Markdown

PR Review: SecretResolver Vault Integration [OMN-1374]

Summary

This PR implements comprehensive Vault integration for SecretResolver with observability features. The implementation demonstrates strong engineering practices with detailed documentation, thread safety considerations, and extensive test coverage.

✅ Strengths

1. Excellent Documentation

  • Comprehensive module-level docstrings with security considerations, memory handling notes
  • Clear explanation of thread safety strategy (RLock + per-key async locks)
  • Detailed comments on edge cases (sync/async race conditions, bootstrap secret isolation)

2. Strong Security Practices

  • Path traversal prevention with proper validation
  • Sanitized error messages that never leak secret values or paths
  • Bootstrap secret isolation to prevent circular dependencies
  • SecretStr wrapping for all returned values

3. Thread Safety & Concurrency

  • Two-level locking strategy well-documented and correctly implemented
  • RLock (reentrant) properly used to avoid deadlock when methods call each other
  • Per-key async locks enable parallel resolution of different secrets

4. Observability & Metrics

  • ModelSecretResolverMetrics model with computed properties (cache_hit_rate, total_resolutions)
  • ProtocolSecretResolverMetrics allows external metrics collector integration
  • Deque-based latency tracking (O(1) rotation)

5. Test Coverage

  • Comprehensive unit tests (70+ tests based on commit message)
  • Integration tests with real Vault server
  • Graceful skip when Vault unavailable (CI-friendly)

🔍 Issues & Concerns

CRITICAL: Potential Memory Leak in _async_key_locks

Location: secret_resolver.py:586-637

Issue: The _async_key_locks dictionary grows unboundedly and is never cleaned up. While the docstring acknowledges this, the mitigation is insufficient.

Why This is Problematic:

  1. Attack Surface: If logical names come from untrusted input, an attacker can exhaust memory by requesting thousands of unique secret names
  2. Production Drift: Even in legitimate use, if the application dynamically generates logical names, the dictionary will grow indefinitely
  3. Warning Once Only: The warning fires only at exactly 1000 entries, not continuously

Recommended Fix - Option 1 (LRU eviction):
Use OrderedDict with max size and LRU eviction when limit reached

Recommended Fix - Option 2 (Validation):
Only create locks for configured secrets (validate against _mappings)


MEDIUM: TTL Jitter Timing

Location: secret_resolver.py:1664-1679

Issue: Jitter is additive only (1.0 to 1.1x). Should be symmetric (±10%) for better cache stampede prevention.

Recommendation:
Use jitter_factor = random.uniform(-0.1, 0.1) with max(1, ...) guard


MEDIUM: Metrics Protocol Thread Safety Docs

Location: secret_resolver.py:169-227

Issue: Protocol mentions thread safety but doesn't specify which primitives are safe.

Recommendation: Add concrete examples of thread-safe implementations (threading.Lock, prometheus_client)


📋 ONEX Pattern Compliance

Pattern Status
No Any types ✅ PASS
Strong typing ✅ PASS
Error handling ✅ PASS
Correlation IDs ✅ PASS
Security patterns ⚠️ PARTIAL (DoS mitigation issue)
Container DI ✅ PASS
Documentation ✅ PASS

🎯 Recommendations

Must Fix (Before Merge)

  1. _async_key_locks memory leak - implement LRU eviction or validation

Should Fix (Before Merge)

  1. TTL jitter - use symmetric jitter (±10%)
  2. Metrics protocol docs - add thread-safety examples

Nice to Have (Follow-up PR)

  1. Extract jitter percentage to constant (0.1 → CACHE_TTL_JITTER_PERCENT)

🏆 Overall Assessment

Grade: A- (Excellent with minor issues)

This is a production-ready implementation with exceptional attention to:

  • Security (path sanitization, secret handling)
  • Observability (metrics, structured logging)
  • Documentation (comprehensive docstrings)
  • Testing (both unit and integration)

Primary concern: Unbounded _async_key_locks dictionary growth should be addressed before merge.

Recommendation: Approve with changes - fix the memory leak, then merge.

Great work on this implementation! 🎉

…r [OMN-1374]

- Add LRU eviction to _async_key_locks to prevent unbounded memory growth
  - Evicts oldest 10% of entries when reaching MAX_ASYNC_KEY_LOCKS (1000)
  - Logs warning on eviction as DoS/misconfiguration indicator
- Change TTL jitter from additive-only (0-10%) to symmetric (±10%)
  - Better cache stampede prevention with spread distribution
  - Add max(1, ...) guard to ensure TTL is never zero
- Add thread-safety documentation to ProtocolSecretResolverMetrics
  - Document recommended primitives (Lock, RLock, Counter, prometheus_client)
  - Add minimal ThreadSafeSecretResolverMetrics example using threading.Lock
- Extract CACHE_TTL_JITTER_PERCENT constant (0.1)
@claude

claude Bot commented Jan 18, 2026

Copy link
Copy Markdown

PR Review: Vault Integration for SecretResolver [OMN-1374]

Summary

This PR implements production-ready Vault integration with comprehensive observability features. The implementation is well-architected with strong attention to thread safety, security, and performance. Overall quality is excellent with only minor suggestions for consideration.

Strengths

Thread Safety - Exceptional

  • RLock usage: Correctly switched from Lock to RLock to handle reentrant scenarios
  • Per-key async locks: Smart design preventing duplicate concurrent fetches for the same secret
  • LRU eviction for async locks: Prevents unbounded memory growth with 10% eviction at 1000 entries
  • Capture-then-use pattern: Metrics collector reference captured under lock, then used outside

Security - Well Thought Out

  • Path sanitization: Vault paths never logged
  • Bootstrap secret isolation: Prevents circular dependency with Vault initialization
  • Error message sanitization: Only logical names exposed
  • Type conversion documentation: Clear explanation of str(value) usage

Observability - Production Ready

  • Comprehensive metrics: Success/failure counts, latency tracking, cache hit rates
  • External metrics protocol: Pluggable ProtocolSecretResolverMetrics with thread-safety docs
  • TTL jitter: ±10% symmetric jitter prevents cache stampede
  • Structured logging: Consistent correlation_id propagation

Test Coverage - Thorough

  • 2,485 total test lines (2,018 unit + 467 integration)
  • Integration tests gracefully skip when Vault unavailable
  • Multiple test classes covering Vault integration, correlation ID, metrics

Documentation - Exemplary

  • Module-level docstring is comprehensive
  • Thread safety contracts documented for every method
  • Security considerations explicitly called out

Code Quality

Type Safety ✓

  • No Any types detected - complies with ONEX strict typing policy
  • Proper use of JsonType for Vault envelopes
  • All parameters and return types properly annotated

Error Handling ✓

  • Proper error hierarchy: InfraAuthenticationError, InfraTimeoutError, InfraUnavailableError, SecretResolutionError
  • Correlation ID propagation through all error contexts
  • Graceful degradation when vault_handler is None

Performance ✓

  • deque with maxlen=1000: O(1) rotation vs O(n) for list.pop(0)
  • Cache TTL jitter: ±10% symmetric distribution prevents stampede
  • Check-before-write pattern: Avoids unnecessary cache overwrites

Minor Suggestions (Non-Blocking)

1. Metrics Collector Error Handling

File: secret_resolver.py:892-902, 954-963

Consider adding circuit breaker for metrics collector if failures become common. Current implementation is acceptable for initial release.

2. Vault Handler Envelope Format

File: secret_resolver.py:1550-1557

Ensure integration tests verify the envelope format contract with HandlerVault to catch interface changes.

3. String Conversion Warning

File: secret_resolver.py:1624-1638

Consider adding warning log when converting non-string Vault values to alert about Python repr vs JSON.

Security Assessment

Well-Designed Security Controls:

  1. SecretStr wrapping prevents accidental logging
  2. Path sanitization - Vault paths never in logs
  3. Bootstrap secret isolation prevents circular dependencies
  4. File size limits (1MB) prevent memory exhaustion
  5. DoS mitigation with warning threshold + LRU eviction

No Critical Vulnerabilities Detected ✓

Performance

Efficient design choices:

  • deque with maxlen for O(1) latency tracking
  • Per-key async locks for parallel resolution
  • TTL jitter prevents cache stampede
  • Check-before-write reduces cache writes

Potential bottleneck (low risk): RLock contention minimal due to brief lock hold times.

Test Coverage ✓

  • Unit tests: 2,018 lines (Vault integration, path parsing, correlation ID, metrics)
  • Integration tests: 467 lines (real Vault, proper isolation, graceful skip)
  • Edge cases: Bootstrap secrets, error handling, thread safety

Optional additional tests:

  • Concurrent async resolution
  • Metrics collector failures
  • LRU eviction behavior

ONEX Compliance ✓

  • No Any types
  • PEP 604 unions (X | None)
  • Pydantic models frozen and strict
  • Correct error hierarchy
  • JsonType usage
  • Comprehensive documentation

Final Verdict

APPROVED ✓

This is excellent work with production-ready quality. The implementation demonstrates:

  • Deep understanding of thread safety and concurrency
  • Strong security awareness
  • Comprehensive observability
  • Thorough testing

The minor suggestions above are non-blocking and can be addressed in follow-up PRs if needed.

Review Checklist

  • ✓ Code quality and best practices
  • ✓ Potential bugs or issues (minor edge cases noted)
  • ✓ Performance considerations (efficient design)
  • ✓ Security concerns (well-designed)
  • ✓ Test coverage (comprehensive)
  • ✓ ONEX compliance (fully compliant)

Great work, @jonahgabriel!

…e contract [OMN-1374]

Add comprehensive integration tests verifying the envelope format contract
between SecretResolver and HandlerVault. This catches interface drift if
either component changes their expected envelope structure.

Tests cover:
- Envelope structure (operation, payload, correlation_id)
- Payload structure (path, mount_point parsing)
- Response parsing and field extraction
- Malformed response handling
- Contract stability documentation
@claude

claude Bot commented Jan 18, 2026

Copy link
Copy Markdown

PR Review: Complete Vault Integration for SecretResolver [OMN-1374]

Overall Assessment

This is an excellent, production-ready implementation with exceptional attention to detail. The code demonstrates strong engineering practices with comprehensive error handling, thorough documentation, extensive test coverage, and careful consideration of security, thread safety, and observability.

Recommendation: Approve with Minor Suggestions


Strengths

1. Exceptional Documentation

  • Module-level docstring is comprehensive (lines 1-121)
  • Every method has detailed docstrings with thread safety notes
  • Edge cases documented (sync/async races, TOCTOU, string conversion)
  • Protocol includes thread-safe implementation examples

2. Security Best Practices

  • Bootstrap secrets isolation prevents circular dependencies
  • Path traversal protection for file-based secrets
  • Sanitized error messages never expose secret values
  • SecretStr wrapping prevents accidental logging
  • File size limits prevent memory exhaustion

3. Thread Safety & Concurrency

  • RLock correctly handles nested locking scenarios
  • Per-key async locks enable parallel resolution
  • Check-before-write pattern prevents race condition overwrites
  • Metrics collector reference captured atomically

4. Observability & Metrics

  • Built-in metrics tracking with external collector support
  • Correlation ID propagation throughout resolution chain
  • Structured logging with appropriate levels
  • Latency tracking uses deque with maxlen for O(1) rotation

5. Resilience Patterns

  • Cache TTL jitter prevents thundering herd
  • LRU eviction prevents unbounded memory growth
  • Graceful degradation when Vault handler is None
  • DoS mitigation with warning thresholds

6. Test Coverage

  • Comprehensive unit tests with mocked Vault handler
  • Integration tests with real Vault (gracefully skip when unavailable)
  • Contract testing validates envelope format (excellent pattern!)
  • Thread safety, metrics, correlation ID tests

Issues & Recommendations

CRITICAL: Type Annotation

Line 1550 uses JsonType for envelope, but dict[str, object] is more appropriate per CLAUDE.md lines 32-35 (JsonDict pattern when dict key access is needed).

HIGH: Missing Container-Based Dependency Injection

Constructor uses explicit parameters instead of ONEX container pattern (lines 398-418). Per CLAUDE.md, should use ModelONEXContainer for DI.

Recommendation: Add factory method from_container() for ONEX integration while keeping current constructor for standalone use.

MEDIUM: LRU Eviction Logging May Be Noisy

Line 694 logs warning on every eviction. In high-throughput systems, this could generate excessive logs. Consider rate limiting (max 1 warning per minute).

MEDIUM: Metrics Collector Errors Use WARNING Level

Lines 895, 956 log metrics errors at WARNING but these are explicitly ignored. Should use DEBUG level since failures don't affect resolution.


Code Quality Observations

Excellent Patterns

  1. Check-Before-Write Cache Pattern (lines 1224-1226, 1279-1281)
  2. Bootstrap Secret Isolation (lines 1132-1140)
  3. Atomic Reference Capture (lines 871-879)
  4. Contract Testing (test_secret_resolver_vault_contract.py)
  5. Symmetric TTL Jitter (lines 1752-1755)

Consistency with ONEX Patterns

  • Error Handling: Uses InfraError hierarchy correctly
  • Naming Conventions: Follows model_.py, Protocol patterns
  • Type Safety: Zero Any types (compliant with CLAUDE.md)
  • Container DI: Missing (see HIGH issue)

Performance Considerations

  1. Global RLock Contention: Lines 494-522 hold lock during I/O (sync path serializes)
  2. Metrics Collector Duck Typing: hasattr() checks on every call (could cache results)

Security Assessment

Strong security posture with proper SecretStr wrapping, path traversal prevention, sanitized errors, file size limits, bootstrap isolation, and TOCTOU prevention.

Recommendations:

  • Add rate limiting for resolution attempts per logical_name
  • Add security event markers for SIEM integration
  • Document Vault token rotation strategy

Conclusion

This PR demonstrates excellent engineering practices and is ready for production.

Action Items

Before Merge (Optional but Recommended):

  1. Fix JsonType to dict[str, object] type annotation (line 1550)
  2. Address container-based DI pattern (add factory method)

Future Improvements:

  1. Add eviction warning rate limiting
  2. Lower metrics collector error log level to DEBUG
  3. Add performance tuning documentation

Approve: Yes, with minor suggestions

Great work! 🎉

Address 4 PR review items:

1. CRITICAL - Type annotation: Change envelope type from JsonType to
   dict[str, object] per CLAUDE.md JsonDict pattern for dict key access

2. HIGH - Container DI: Add from_container() async factory method for
   ONEX container-based dependency injection while preserving standalone
   constructor for direct instantiation

3. MEDIUM - Log noise: Rate-limit LRU eviction warnings to max 1 per
   minute using time.monotonic() to prevent log flooding in high-throughput

4. MEDIUM - Log level: Change metrics collector error logs from WARNING
   to DEBUG since failures are explicitly ignored and non-fatal
@claude

claude Bot commented Jan 18, 2026

Copy link
Copy Markdown

PR Review: SecretResolver Vault Integration [OMN-1374]

Summary

This PR implements production-ready Vault integration for SecretResolver with comprehensive observability features. The implementation is thorough, well-documented, and includes extensive test coverage. However, there are several areas where the code deviates from ONEX infrastructure patterns documented in CLAUDE.md.


🔴 CRITICAL Issues

1. Missing Container-Based Dependency Injection Pattern

Location: secret_resolver.py:403-423

Issue: While the PR adds from_container() factory method (lines 467-616), the constructor still violates ONEX DI patterns by accepting direct dependency parameters instead of using ModelONEXContainer.

ONEX Pattern (from CLAUDE.md):

# CORRECT - Container injection
class MyOrchestrator(NodeOrchestrator):
    def __init__(self, container: ModelONEXContainer) -> None:
        super().__init__(container)

Current Implementation:

# INCORRECT - Direct dependency injection
def __init__(
    self,
    config: ModelSecretResolverConfig,
    vault_handler: HandlerVault | None = None,
    metrics_collector: ProtocolSecretResolverMetrics | None = None,
) -> None:

Recommendation:

  • Change constructor to accept container: ModelONEXContainer
  • Resolve vault_handler and metrics_collector from container in __init__
  • Keep config as parameter (application-specific, cannot be auto-discovered per factory docstring reasoning)
  • Make from_container() the canonical construction pattern, keep old constructor for backward compatibility with deprecation warning

Why This Matters: CLAUDE.md states "All services MUST use ModelONEXContainer for dependency injection." The current pattern creates inconsistency across the codebase and bypasses centralized lifecycle management.


2. Vault Envelope Type Annotation Uses dict[str, object] Instead of Typed Models

Location: secret_resolver.py:1736-1743

Issue: The Vault envelope is hand-constructed as a dict[str, object] rather than using a typed Pydantic model.

Current Implementation:

envelope: dict[str, object] = {
    "operation": "vault.read_secret",
    "payload": {
        "path": vault_path,
        "mount_point": mount_point,
    },
    "correlation_id": str(effective_correlation_id),
}

ONEX Pattern: "Pydantic Models - All data structures must be proper Pydantic models"

Recommendation:

  • Create ModelVaultReadSecretRequest Pydantic model
  • Create nested ModelVaultReadSecretPayload model
  • Use typed models throughout envelope construction
  • This would make contract violations compile-time errors instead of runtime errors

Example:

class ModelVaultReadSecretPayload(BaseModel):
    path: str
    mount_point: str = Field(default="secret")

class ModelVaultReadSecretRequest(BaseModel):
    operation: Literal["vault.read_secret"]
    payload: ModelVaultReadSecretPayload
    correlation_id: UUID

Why This Matters: The extensive contract testing in test_secret_resolver_vault_contract.py (833 lines!) wouldn't be necessary if the envelope was a typed model - Pydantic would enforce the contract at construction time.


3. Any Type Policy Compliance - Potential Hidden Violations

Location: Throughout codebase

Issue: While the visible code avoids Any types correctly, the AST validator has known limitations (per CLAUDE.md):

CLAUDE.md Warning:

What is NOT Detected (validator limitations):

  1. External type aliases: When Any is hidden behind an imported alias
  2. Runtime type construction: Factory patterns creating types dynamically

Recommendation:

  • Audit all imports from omnibase_core for hidden Any usage
  • Specifically check ModelHandlerOutput[dict[str, object]] - verify the generic parameter doesn't resolve to Any internally
  • Run poetry run python scripts/validate.py any_types locally to confirm

⚠️ HIGH Priority Issues

4. Thread Safety - Lock Granularity May Cause Latency Issues

Location: secret_resolver.py:656-684 (sync get_secret)

Issue: The sync get_secret() method holds _lock for the entire resolution operation, including potentially slow I/O (Vault API calls, file reads).

Code Analysis:

def get_secret(...) -> SecretStr | None:
    with self._lock:  # Lock held for entire operation
        cached = self._get_from_cache(logical_name)
        if cached is not None:
            return cached
        
        # PROBLEM: I/O operations under lock
        result = self._resolve_secret(logical_name, ...)  # May call Vault/file I/O
        # ... cache write ...

Impact: During a slow Vault resolution (e.g., 500ms timeout), all other threads are blocked from even checking the cache. This can cause cascading latency under load.

Async Implementation (lines 717-796) is better - it releases lock during I/O and uses per-key locks.

Recommendation:

  • Refactor sync method to use same pattern as async: lock only for cache access, release during I/O
  • Document why sync version has different locking behavior (if intentional)
  • Consider deprecating sync method in favor of async-only API

5. LRU Eviction Logic Has Edge Case Bug

Location: secret_resolver.py:883-891

Issue: LRU eviction uses dict insertion order but doesn't account for the key being added after eviction.

Code:

if lock_count >= MAX_ASYNC_KEY_LOCKS:
    evict_count = max(1, MAX_ASYNC_KEY_LOCKS // 10)  # 10%, minimum 1
    keys_to_evict = list(self._async_key_locks.keys())[:evict_count]
    for key in keys_to_evict:
        del self._async_key_locks[key]
    # Rate-limited warning
    self._maybe_log_eviction_warning(evict_count)

self._async_key_locks[logical_name] = asyncio.Lock()  # Added after eviction

Edge Case: When lock_count == MAX_ASYNC_KEY_LOCKS (exactly 1000), the eviction removes 100 entries, bringing count to 900, then adds 1, resulting in 901. This is fine. But the logic description says "when reaching MAX_ASYNC_KEY_LOCKS" which implies it should stay at or below the max.

Actual Behavior: Dict can grow to MAX_ASYNC_KEY_LOCKS + 1 briefly before next eviction.

Recommendation:

  • Change condition to if lock_count >= MAX_ASYNC_KEY_LOCKS - 1: to ensure dict never exceeds max
  • OR: Update docstring to clarify dict can briefly exceed max by 1
  • OR: Evict before checking if add would exceed limit

6. Error Context Transport Type Inconsistency

Location: secret_resolver.py:563-570, 671-678, etc.

Issue: from_container() uses EnumInfraTransportType.RUNTIME but resolution errors use EnumInfraTransportType.VAULT.

Code:

# from_container() error context
context = ModelInfraErrorContext.with_correlation(
    transport_type=EnumInfraTransportType.RUNTIME,  # RUNTIME
    operation="from_container",
    ...
)

# _resolve_secret_async() error context  
context = ModelInfraErrorContext.with_correlation(
    transport_type=EnumInfraTransportType.VAULT,  # VAULT
    operation="read_secret",
    ...
)

Issue: What should be used for env/file resolution errors? Currently uses the source type (vault, env, file) but there's no EnumInfraTransportType.ENV or EnumInfraTransportType.FILE.

Recommendation:

  • Use RUNTIME for all SecretResolver operations (since it's a runtime service)
  • Add source_type to error context metadata instead of using transport_type
  • OR: Add new transport types ENV, FILE to enum

⚙️ MEDIUM Priority Issues

7. Magic Numbers Should Be Constants

Location: Multiple locations

Examples:

  • Line 941: ttl_seconds = max(1, int(...)) - hardcoded minimum TTL of 1 second
  • Line 1576: f.read(MAX_SECRET_FILE_SIZE + 1) - the + 1 is magic
  • Line 884: MAX_ASYNC_KEY_LOCKS // 10 - hardcoded 10% eviction ratio

Recommendation: Extract constants:

MIN_CACHE_TTL_SECONDS = 1
LRU_EVICTION_RATIO = 0.1  # 10% of entries

8. Path Traversal Detection Has False Negatives

Location: secret_resolver.py:1556-1568

Issue: The path traversal check only applies to relative paths, not absolute paths.

Code:

# SECURITY: Prevent path traversal attacks
# Verify the resolved path is within secrets_dir for relative paths
# Absolute paths are trusted (explicitly configured by administrator)
if original_is_relative:
    secrets_dir_resolved = self._config.secrets_dir.resolve()
    if not resolved_path.is_relative_to(secrets_dir_resolved):
        logger.error("Path traversal detected for secret: %s", logical_name)
        return None

Vulnerability: If configuration allows absolute paths (e.g., via explicit mappings), an attacker with config write access could point to sensitive files like /etc/shadow, /root/.ssh/id_rsa.

Recommendation:

  • Apply path traversal check to all paths, including absolute
  • Define an allowed root directory for absolute paths (e.g., /run/secrets, /etc/secrets)
  • OR: Document security assumption: "Configuration is trusted. Only administrators should have config write access."

9. Bootstrap Secret Resolution Bypasses Convention Fallback Incorrectly

Location: secret_resolver.py:1316-1326

Issue: Bootstrap secrets always use convention-based naming, even if explicit mapping exists.

Code:

if self._is_bootstrap_secret(logical_name):
    secret = self._resolve_bootstrap_secret_value(logical_name)  # Always uses convention
    # ...

Problem: If a user explicitly maps vault.token to a custom env var name in config, it's ignored for bootstrap secrets.

Expected Behavior: Check explicit mapping first, fall back to convention only if no mapping exists.

Recommendation:

if self._is_bootstrap_secret(logical_name):
    # Try explicit mapping first
    if logical_name in self._mappings:
        source = self._mappings[logical_name]
        if source.source_type == "env":
            secret = os.environ.get(source.source_path)
        else:
            logger.warning("Bootstrap secret must be env type, got: %s", source.source_type)
            secret = None
    else:
        # Fall back to convention
        secret = self._resolve_bootstrap_secret_value(logical_name)

10. Metrics Collector Errors Logged at Wrong Level

Location: secret_resolver.py:1078-1088, 1139-1149

Current: Metrics collector errors logged at DEBUG level (changed from WARNING per commit message).

Reasoning from code comment: "log at debug level since these are explicitly ignored and non-fatal"

Counterargument: If metrics collection is enabled via explicit metrics_collector injection, failures should be visible. DEBUG logs are often disabled in production, causing silent metric loss.

Recommendation:

  • Log at WARNING level with rate limiting (max 1 per minute)
  • Add counter for suppressed warnings
  • Document in collector protocol: "Implementations should be resilient, but exceptions will be logged at WARNING level"

✅ STRENGTHS

What This PR Does Well

  1. Comprehensive Documentation: Docstrings are exceptional - thread safety, security considerations, edge cases all documented
  2. Test Coverage: 833 lines of contract tests + integration tests + unit tests
  3. Security Mindset:
    • Path traversal protection
    • Secret sanitization in logs
    • Bootstrap secret isolation
  4. Observability: Metrics tracking, structured logging, correlation IDs
  5. Graceful Degradation: Vault unavailable doesn't crash the system
  6. Thread Safety: Dual-lock strategy (RLock + per-key async locks) is well-designed
  7. Error Handling: Proper error hierarchy usage (InfraAuthenticationError, InfraTimeoutError, etc.)

📋 RECOMMENDATIONS

Must-Fix (Before Merge)

  1. ✅ Container-based DI - Align constructor with ONEX pattern
  2. ✅ Typed Envelope Models - Replace dict[str, object] with Pydantic models
  3. ✅ Lock Granularity - Document or fix sync method holding lock during I/O

Should-Fix (Can Be Follow-up)

  1. LRU eviction edge case
  2. Error context transport type consistency
  3. Bootstrap secret mapping override
  4. Magic number extraction

Nice-to-Have (Low Priority)

  1. Path traversal for absolute paths
  2. Metrics error logging level
  3. Deprecation of sync API in favor of async-only

🎯 ONEX Compliance Checklist

  • ❌ Container Injection: Constructor should accept ModelONEXContainer
  • ✅ Strong Typing: No Any types (verified via docstrings and code inspection)
  • ❌ Pydantic Models: Vault envelope uses dict instead of typed model
  • ✅ PEP 604 Unions: Uses X | None consistently
  • ✅ Error Hierarchy: Proper use of InfraError subclasses
  • ✅ Correlation IDs: Propagated throughout call chain
  • ✅ Security: Sanitized error messages, no secret logging

📊 Metrics

  • Files Changed: 6 (+3012 lines, -139 lines)
  • Test Coverage: ~1800 lines of tests (unit + integration + contract)
  • Documentation Quality: Excellent (comprehensive docstrings with security/threading notes)
  • Code Complexity: High (2000+ line file, complex threading model)

✍️ Final Verdict

Approval Status: ⚠️ Conditional Approval (pending critical fixes)

This is high-quality production code with excellent test coverage and documentation. However, critical deviations from ONEX patterns (container-based DI, typed models) must be addressed before merge to maintain codebase consistency.

Suggested Action:

  1. Address CRITICAL issues feat: PostgreSQL Adapter with Comprehensive Tests and Structured Logging #1 and Add Claude Code GitHub Workflow #2
  2. Document sync lock behavior (issue feat: Complete infrastructure containers operational with Docker secrets #4) if intentional
  3. Merge with follow-up ticket for SHOULD-FIX items

Great work on the Vault integration! The observability features and security considerations are particularly well-executed. 🚀


Reviewed by: Claude Sonnet 4.5
Date: 2026-01-18

…OMN-1374]

- Use JsonType for Vault envelopes instead of dict[str, object]
- Fix bootstrap secret resolution to respect explicit env mappings
- Elevate metrics collector error log level from debug to warning
- Add tests for bootstrap secret explicit mapping behavior
@claude

claude Bot commented Jan 18, 2026

Copy link
Copy Markdown

PR Review: Vault Integration for SecretResolver [OMN-1374]

Overall Assessment

Verdict: APPROVE with minor recommendations

This is a well-architected, production-ready implementation of Vault integration for SecretResolver. The code demonstrates excellent attention to security, observability, thread safety, and testing.


Strengths

1. Security-First Design ✅

  • No secrets in logs: Path sanitization prevents leaking Vault structure
  • SecretStr wrapping: All values wrapped immediately after resolution
  • Path traversal protection: Robust validation for file-based secrets
  • Bootstrap secret isolation: Prevents circular dependencies during Vault init
  • Memory safety documentation: Clear guidance on secure memory handling limitations

2. Excellent Thread Safety ✅

  • Two-level locking strategy: threading.RLock for cache + per-key asyncio.Lock for async deduplication
  • Sync/async coordination: Well-documented edge case handling
  • LRU eviction: Memory leak prevention for async key locks with rate-limited warnings
  • Race condition handling: Check-before-write pattern documented

3. Comprehensive Observability ✅

  • Metrics model (ModelSecretResolverMetrics): Tracks success/failure by source type
  • Protocol-based metrics collection: ProtocolSecretResolverMetrics for external collectors
  • Latency tracking: Rolling window with deque (max 1000 samples)
  • Structured logging: Correlation IDs, source types, latencies

4. Production-Ready Error Handling ✅

  • Graceful degradation: When vault_handler is None, returns None with warning
  • Proper error context: All errors include ModelInfraErrorContext with correlation IDs
  • Sanitized error messages: No secret values or paths in error messages
  • Type-specific handling: InfraAuthenticationError, InfraTimeoutError, InfraUnavailableError

5. Testing Excellence ✅

  • Unit tests: Comprehensive mocking of Vault handler (593 new lines)
  • Integration tests: Real Vault integration with graceful CI skipping (467 lines)
  • Contract tests: Envelope format validation between SecretResolver and HandlerVault (833 lines)
  • Test isolation: Unique secret paths, cleanup fixtures, no test pollution

Architecture Highlights

Path Parsing Excellence

The _parse_vault_path_components() method handles edge cases (no slash, no field) and warns on unusual formats (empty path segment).

Cache TTL Jitter

# ±10% symmetric jitter prevents cache stampede
jitter_factor = random.uniform(-CACHE_TTL_JITTER_PERCENT, CACHE_TTL_JITTER_PERCENT)
ttl_seconds = max(1, int(base_ttl_seconds * (1 + jitter_factor)))

Prevents thundering herd when many entries expire simultaneously.

DoS Mitigation

# Warning threshold + LRU eviction for async key locks
if lock_count >= MAX_ASYNC_KEY_LOCKS:
    evict_count = max(1, MAX_ASYNC_KEY_LOCKS // 10)  # Evict oldest 10%

Rate-limited warnings (max 1/minute) with graceful degradation.


Recommendations (All Minor)

1. Type Conversion Documentation

Location: secret_resolver.py:1832-1846

The str() conversion for Vault values is intentional but could surprise users with complex types. Consider adding a validation example in the docstring:

# Example validation pattern for structured data:
# value_str = resolver.get_secret("config.json")
# config = json.loads(value_str.get_secret_value())

Priority: Low - Current documentation is clear.

2. Bootstrap Secret Documentation

Location: secret_resolver.py:1250-1268

The bootstrap secret isolation is critical but could be more discoverable. Consider adding a prominent note in the class docstring listing which secrets bypass normal resolution.

Priority: Low - Already well-documented in method docstrings.

3. Async Context Detection Enhancement

Location: secret_resolver.py:1682-1700

The sync _read_vault_secret_sync correctly detects async context and raises an error. Consider logging at DEBUG level when falling through to asyncio.run() for awareness.

Priority: Very Low - Current behavior is correct.


Security Audit

✅ Passed

  • No hardcoded secrets
  • No secrets in logs (SecretStr wrapping)
  • Path traversal protection (file secrets)
  • Vault path sanitization (never logged)
  • Error message sanitization
  • Bootstrap secret isolation
  • Memory safety documented

Performance Analysis

Strengths

  • Parallel async resolution: get_secrets_async() uses asyncio.gather()
  • Per-key locking: Different secrets fetch in parallel
  • LRU eviction: Bounded memory for async locks (max 1000)
  • Deque for latency samples: O(1) rotation vs O(n) for list

Potential Concern (Documented)

  • Sync Vault calls: _read_vault_secret_sync creates a new event loop with asyncio.run()
    • Impact: Slight overhead, but acceptable for sync API
    • Mitigation: Users should prefer get_secret_async() for Vault secrets
    • Documented: Warning in get_secret() docstring

Test Coverage Analysis

Test Type Lines Coverage Focus
Unit 593 Mocked Vault handler, edge cases, threading
Integration 467 Real Vault, error scenarios, correlation IDs
Contract 833 Envelope format validation

Strengths:

  • Integration test isolation with unique secret paths and cleanup fixtures
  • Graceful CI degradation when Vault unavailable
  • Contract tests catch interface drift between SecretResolver and HandlerVault

ONEX Compliance Checklist

Requirement Status Notes
No Any types ✅ All type annotations use object, JsonType, or specific types
PEP 604 unions ✅ Uses X | None throughout
Strong typing ✅ ModelSecretResolverMetrics, ModelInfraErrorContext, SecretStr
Error hierarchy ✅ Uses SecretResolutionError, InfraAuthenticationError, etc.
Correlation IDs ✅ Propagated through entire resolution chain
Protocol-based ✅ ProtocolSecretResolverMetrics for external collectors
Container injection ✅ from_container() factory method

Final Recommendation

APPROVE ✅

This PR demonstrates:

  • Production readiness: Security, error handling, observability
  • Excellent engineering: Thread safety, testing, documentation
  • ONEX compliance: Strong typing, no Any, protocol-based
  • Future-proof: Metrics protocol, graceful degradation, container integration

The minor recommendations are enhancements, not blockers. This code is ready to merge.


Code Metrics

Metric Value Assessment
Lines Added 3103 Large but justified (1094 impl + 2009 tests)
Lines Deleted 143 Clean refactoring
Files Changed 6 Focused scope
Cyclomatic Complexity Low-Medium Well-factored methods
Documentation Ratio High ~30% docstrings/comments

Reviewed by: Claude Code
Date: 2026-01-18
Confidence: High (comprehensive review of 3103 lines)

@jonahgabriel
jonahgabriel merged commit c644728 into main Jan 18, 2026
20 checks passed
@jonahgabriel
jonahgabriel deleted the jonah/omn-1374-secretresolver-complete-vault-integration-and-add branch January 18, 2026 16:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant