Skip to content

Architecture Decision Record (ADR)

Title

Iterative URI Resolution with Cycle Detection

Status

Accepted

Date

2025-10-13

Context

Secret URIs may resolve to values containing other URIs or variables requiring further expansion. This occurs in scenarios like:

  • Gradual migration: akv://vault/old-name"akv://vault/new-name" → actual secret
  • Multi-level indirection for access control or configuration management
  • Variable expansion in resolved values: akv://vault/indirect"akv://vault/${KEY}" → needs expansion

Without iterative resolution, users would need manual multi-step resolution. We need to maintain idempotency (plain strings return unchanged) while detecting circular references.

Decision

Implement iterative resolution in SecretResolver.resolve() with cycle detection using a seen set:

seen = set()
current = uri

while True:
    if current in seen:
        raise CircularReferenceError(variable_name=current, chain=[*list(seen), current])
    seen.add(current)

    expanded = expand_variables(current, env)
    if not is_secret_uri(expanded):
        return expanded  # Termination: not a URI

    resolved = provider.resolve(parse_secret_uri(expanded))
    if resolved == current:
        return resolved  # Termination: stable value

    current = resolved

Rationale

  • Idempotency: Plain strings and non-target URIs return immediately without provider calls
  • Flexibility: Supports arbitrary nesting depth and mixed variable/URI resolution
  • Safety: seen set guarantees cycle detection without infinite loops
  • Simplicity: Single resolution entry point handles all cases uniformly

Implications

Positive Implications

  • Users can chain URIs across vaults for access control patterns
  • Variable expansion works at any nesting level
  • Backward compatibility: existing single-level URIs work unchanged
  • Idempotency ensures safe repeated calls

Concerns

  • Performance: Multiple provider calls increase latency. Mitigation: Future TTL cache (ADR-pending)
  • Debugging: Long resolution chains are hard to trace. Mitigation: CircularReferenceError includes full chain
  • Complexity: Harder to reason about multi-step resolution. Mitigation: Comprehensive E2E tests (7 test cases)

Alternatives

1. Single-pass resolution only

  • Simpler implementation
  • Rejected: Users would need manual multi-step calls

2. Recursive resolution

  • More functional style
  • Rejected: Stack overflow risk, harder to track seen values

3. Fixed depth limit (e.g., max 10 iterations)

  • Simpler termination logic
  • Rejected: Arbitrary limit; cycle detection is more robust

Future Direction

  • Add resolution metrics/logging for observability
  • Consider optional depth limit as safety net (configurable, default disabled)
  • Implement TTL cache to reduce redundant provider calls
  • Evaluate async resolution for parallel multi-vault lookups

References

  • E2E tests: tests/e2e/test_nested_resolution.py
  • Unit tests: tests/unit/test_resolver.py
  • Implementation: src/envresolve/application/resolver.py:34-95