Extending hostctl
Host is the operational contract; HostConfig owns secret-safe connection
identity, URI selection, and lifecycle. Installed integrations register config
subclasses through the hostctl.configs entry-point group. Registry hooks are
protected implementation details.
An API client and a transport are usually different objects. Prefer composition:
from hostctl import SshConfig
class ManagementClient:
def __init__(self, api, host_config):
self.api = api
self.host_config = host_config
def deploy(self, payload):
with self.host_config as host:
host.path("/tmp/payload").write_bytes(payload)
return host.run(["install", "/tmp/payload"], check=True)
client = ManagementClient(api=management_api, host_config=SshConfig("server"))
Subclass a concrete host only when the machine itself has additional transport behavior—for example, a management WebSocket filesystem fallback:
from hostctl import PosixHost, SshConfig
class ManagedHost(PosixHost):
def path(self, *segments, backend=None):
if backend in (None, "sftp"):
return super().path(*segments, backend=backend)
if backend == "management":
return ManagementPath(self.management_client, *segments)
raise ValueError(f"unknown path backend: {backend}")
Such a path must implement the pathlib_next.Path contract. Advertise only
usable operations in capabilities; unsupported base run() and path() calls
raise NotImplementedError. Connection URIs never contain secrets—pass those
separately to Host(connection_string, **options).
To add a new access mechanism rather than new host behavior, write an
ExecutorProvider or PathProvider instead of subclassing a host — see
Systems and providers for the authoring contract, per-operation
capabilities, and the no-replay safety rule.
Both with config as host: and with config.open() as host: connect the
transport and guarantee close() on normal exit or exceptions. Hosts also
support their own context manager, connect(), and close() lifecycle.