Skip to content

Capability Proxy Architecture (Design Rationale)

This document explains the design of Neops’ capability-based connection proxy system: why it exists, how it works, and what trade-offs it makes.

If you want to use the system, start with:

  • docs/development/30-device-connections.md
  • docs/development/31-connection-methods.md
  • docs/development/32-method-resolution-priority.md

Problem statement

The connection proxy system needs to:

  1. Wrap connection libraries (SSH, HTTP, Scrapli, Netmiko, etc.) for different platforms
  2. Let users extend behavior via plugins
  3. Delegate method calls to the resolved plugin at runtime
  4. Make missing functionality explicit (clear errors + rich context)
  5. Provide good IDE typing for network engineers

Previous approach issues (why “fallbacks classes” were removed)

The older “fallbacks” pattern was counterintuitive:

# Old approach - confusing semantics
class MyConnectionProxyFallbacks(ConnectionProxyInterface, InventoryPluginInterface):
    def get_inventory(self) -> list[str]:
        raise NotImplementedForThisPlatform("This method is not implemented")

class MyConnectionProxy(ConnectionProxy, MyConnectionProxyFallbacks):
    pass

Problems:

  • “Fallbacks” suggested alternative implementations, but were mostly “failure defaults”
  • You ended up with a confusing hierarchy (Proxy → Fallbacks → Interface)
  • It wasn’t obvious what would fail at runtime until you hit it in production

Solution: metaclass-based capability system

High-level idea:

  • A proxy declares required capabilities via inheritance (interfaces with @abstractmethod)
  • Plugins implement capability methods
  • A metaclass generates fallback methods so proxies are still instantiable

Core components (current implementation)

  • CapabilityInterface: base class for capability interfaces (neops_worker_sdk/connection/capabilities/base.py)
  • ProxyMeta: metaclass that:
  • finds capability interfaces via inheritance
  • collects their abstract methods
  • injects fallback implementations (raising NotImplementedForThisPlatform)
  • clears __abstractmethods__ so the proxy is instantiable
  • ConnectionProxy: user-facing base proxy (delegates capability calls to plugin methods)
  • NotImplementedForThisPlatform: exception that includes rich context (proxy, method, interface, platform, plugin, device id)

Note: Capability interfaces are recognized by inheritance (subclasses of CapabilityInterface). There is no separate “marker attribute” required.


Delegation model (what happens on a call)

ConnectionProxy.__getattribute__ intercepts attribute access and delegates only capability methods:

  1. Private attributes (prefix _) are returned from the proxy instance
  2. If the attribute name is a known capability method, the proxy returns the plugin’s method if implemented
  3. Otherwise, the proxy falls back to itself (including metaclass-injected fallbacks)

Result:

  • if the plugin implements the method, it is executed
  • if it does not, the fallback raises NotImplementedForThisPlatform with context

Plugin selection rules (summary)

Resolution is performed by resolve_plugin(...) in neops_worker_sdk/connection/registry.py:

  • First filters candidates by platform / connection type / library
  • Then filters by required capabilities
  • If no plugin implements all required capabilities:
  • selects the best-match plugin (implements the most required capabilities)
  • logs a warning listing missing capabilities and other candidates
  • if there is a best-match tie → AmbiguousPluginError
  • If multiple candidates remain and fallback_to_default=True:
  • tries connection-type default first (platform + type), then platform default
  • otherwise → AmbiguousPluginError

IDE/type checker note: “proxy is abstract”

Capability interfaces use @abstractmethod, so static analyzers may complain that a proxy “doesn’t implement abstract methods”.

At runtime, this is still valid Python:

  • ProxyMeta injects concrete fallback implementations at class creation time
  • ProxyMeta clears __abstractmethods__

If your IDE complains, you may need to suppress the warning (e.g. PyCharm # noinspection PyAbstractClass) while keeping the runtime behavior unchanged.


Lazy imports (dependency strategy)

Base plugins lazy-import third-party libraries inside BaseConnection.connect() so missing dependencies fail only when the plugin is actually used.

This keeps module import clean (especially for workers that don’t use all connection libraries).


Future considerations (not implemented)

One idea that may be explored in the future is fallback-aware selection:

  • if a proxy overrides a capability method (custom fallback), a plugin missing that method is less risky
  • selection could prefer plugins whose “missing” methods are already covered by proxy overrides

This is not implemented today; treat it as a design discussion rather than current behavior.


Further reading

  • neops_worker_sdk/connection/proxy.py
  • neops_worker_sdk/connection/meta.py
  • neops_worker_sdk/connection/registry.py
  • neops_worker_sdk/connection/draft_usage.py