Chapter 3 — Coordinators, Agents, ProtocolExtensions, Features
Four words. Each has a precise meaning. Casual usage of "extension" or "agent" is the source of most reader confusion in distributed automation. This chapter pins down the vocabulary with code anchors and explains the full Coordinator → Agent → ProtocolExtension → Device chain.
The framework's device-control machinery has four named parts. Each one does one thing; mixing them up makes the code hard to read and the documentation harder to write. This chapter establishes the vocabulary the rest of the volume — and indeed the rest of the series — uses uniformly.
The four words
| Term | Role |
|---|---|
| Coordinator | Manages a class of devices and attaches Agents to them |
| Agent | Speaks a wire protocol |
| ProtocolExtension | Binds a specific Agent to a specific Device, exposing the protocol surface |
| Feature | A capability declared on a Device in the Landscape file |
The full layering for a service-reference Feature:
Landscape file
├── services section
│ └── named service (e.g. serial-concentrator at 10.0.0.50)
└── devices section
└── device-1
└── features tag: { serial: serial-concentrator: 1 }
↓ at runtime
device-1 instance
├── managed by Serial Coordinator
└── .serial → ProtocolExtension
└── wraps → Serial Agent
└── connects to serial-concentrator port 1
Each layer has a class. Each class has a job. Each job is distinct from the others. The rest of the chapter walks each class.
Coordinators
File: codecraftai-landscaping/source/packages/ccai/landscaping/coordinatorbase.py:32
class CoordinatorBase:
"""
Base class for all coordinators.
"""
# Abstract methods (line 148–182)
def activate(self, activation_params):
raise AbstractMethodError(...)
def configure_devices(self, landscape):
raise AbstractMethodError(...)
def establish_connectivity(self, activation_params):
raise AbstractMethodError(...)
A Coordinator owns a class of devices and is responsible for them through the activation lifecycle. The base class is abstract; subclasses implement:
activate(activation_params)— bring the coordinator's devices online. Probe each device, attach its ProtocolExtensions, populate metadata.configure_devices(landscape)— read the device configurations from the landscape and prepare per-device state.establish_connectivity(activation_params)— verify each device responds. Returns(host, ipaddr, status, stdout, stderr, exc)tuples per device (Volume 2 chapter 6 walked this).
Three Coordinator hierarchies in the framework:
ClientCoordinatorBase
File: codecraftai-landscaping/source/packages/ccai/landscaping/clients/clientcoordinatorbase.py:55
For apod clients — the runners and controllers. The Coordinator manages the framework's own machines, not the system under test.
ServiceCoordinatorBase
File: codecraftai-landscaping/source/packages/ccai/landscaping/services/servicecoordinatorbase.py:57
For infrastructure services — the named services from
the Landscape's services section. Per-serviceType
subclasses handle different service types.
NodeCoordinatorBase
File: codecraftai-landscaping/source/packages/ccai/landscaping/clusters/nodecoordinatorbase.py:57
For cluster nodes — devices that participate in clusters
declared by the Topology (chapter 5). The Coordinator
materialises LandscapeDeviceCluster instances from the
Topology's clusters section (Volume 2 chapter 6 walked
the materialisation; chapter 9 of this volume walks the
result).
Protocol-specific subclasses
Concrete Coordinator classes for specific deviceTypes:
SshCoordinatoratcoordinators/ssh/sshcoordinator.py:53. Handlesnetwork/sshdeviceTypes. Itsestablish_connectivity(line 137–156) executes a remote echo command viadevice.ssh.run_cmd()and collects results.SerialCoordinatorBaseatcoordinators/serial/serialcoordinatorbase.py:26. Handles serial-protocol devices.
Each protocol-specific Coordinator's create_landscape_device
method (e.g. sshcoordinator.py:89–135) instantiates the
LandscapeDevice, wires up the protocol's Agent, and
attaches the matching ProtocolExtension.
Coordinator couplings — the registration mechanism
A Coordinator subclass doesn't just exist — it has to be discoverable by the framework. The discovery mechanism is the CoordinatorCoupling.
Example: coordinators/ssh/sshcoordinatorcoupling.py:44
class SshCoordinatorCoupling(CoordinatorCoupling):
"""
The SshCoordinatorCoupling handle the requirement registration for the SSH coordinator.
"""
pathbase = "/ssh"
integration_root: str = "apod"
integration_section: str = "devices"
integration_leaf: str = "deviceType"
integration_class: str = INTEGRATION_CLASS_NETWORK_SSH
@classmethod
def attach_to_environment(cls, landscape: "Landscape"):
"""
This API is called so that the IntegrationCoupling can
process configuration information.
"""
A coupling is declarative metadata:
integration_root—"apod"or"infrastructure"(which Landscape section).integration_section—"devices"or"services".integration_leaf—"deviceType"or"serviceType".integration_class— the string the deviceType / serviceType is matched against ("network/ssh"for SSH).
When the framework activates the integration layer, it walks
the installed couplings, matches each device's deviceType
against the couplings' integration_class, and uses the
matched coupling's COORDINATOR_TYPE to instantiate the
right Coordinator.
The coupling is stateless metadata; the Coordinator is stateful runtime. One coupling type → one Coordinator type. A project adding a new deviceType writes a Coupling subclass and a Coordinator subclass; both are registered through the factory protocol from Volume 0 chapter 7.
Agents
The framework's Agent term is loose in the source code —
different protocols call them different things. The
canonical example, SshAgent from codecraftai-protocol-ssh
(Volume 5), wraps a per-host SSH connection state and vends
SshSession instances tailored to specific tasks (Volume 2
chapter 10 walked the SSH side).
For the purpose of Volume 6's vocabulary:
An Agent is the protocol-speaking object. Given a host address and credentials, it knows how to talk the wire protocol.
Agents are not directly attached to LandscapeDevice
instances. A ProtocolExtension wraps an Agent; the device
exposes the ProtocolExtension. This separation lets the
ProtocolExtension expose a device-level surface
(device.ssh.run_cmd) while the Agent does the actual
protocol work (open SSH session, exchange packets, return
results).
ProtocolExtensions
File: codecraftai-landscaping/source/packages/ccai/landscaping/extensions/protocol/protocolextension.py:28
class ProtocolExtension:
"""
... allows for the extension of functionality for a
:class:`LandscapeDevice` or :class:`LandscapeService`.
The :class:`ProtocolExtension` is used by the UPnP, SSH
and Other coordinators to extend landscape devices with
metadata and device connectivity for the UPnP, SSH and
Other protocols.
"""
def __init__(self):
self._coord_ref = None
self._extends_ref = None
self._configinfo = None
self._extid = None
self._location = None
def initialize(self, coord_ref, extends_ref, extid, location, configinfo, **kwargs):
...
A ProtocolExtension:
- Holds a weak reference to its owning Coordinator
(
_coord_ref). - Holds a weak reference to the Device or Service it extends
(
_extends_ref). - Stores the protocol-specific configuration (
_configinfo), the extension's unique identifier (_extid), and the network location (_location). - Is attached to a Device via
device.attach_extension(name, ext)— the device's_extensionsdict at line 113 oflandscapedevice.pycarries the attachment.
A ProtocolExtension subclass implements the device-level
methods the test calls — run_cmd on an SSH extension,
write and read on a serial extension, cycle on a
power extension. Each method is a thin wrapper around the
underlying Agent's protocol calls.
The Coordinator is responsible for creating the
ProtocolExtension, initializing it with the right Agent and
configuration, and attaching it to the Device. The Device
is responsible for exposing the ProtocolExtension as a
device-level attribute (device.ssh).
A ProtocolExtension is not a BaseExt
A point of vocabulary worth pinning down: ProtocolExtension
is not the same as BaseExt (chapter 8). They serve
different purposes:
ProtocolExtensionextends aLandscapeDevicewith protocol connectivity. The most common kind of extension.BaseExtis a service-specific framework-extension class (e.g., the vSphere extension model). It extends a service's behaviour, not a device's protocol surface.
Chapter 8 walks BaseExt in detail. When the volume says
"extension" without qualification, the chapter context will
disambiguate, but readers should keep the distinction in mind.
ControlExtension — the service control point
File: codecraftai-landscaping/source/packages/ccai/landscaping/extensions/control/controlextension.py:27
class ControlExtension:
"""
... allows for addition of controls to extend the
functionality of a :class:`LandscapeDevice`. The
:class:`ControlExtension` is used by the various services
to link controls back to services.
"""
def __init__(self):
self._service_ref = None
self._extends_ref = None
self._control_id = None
self._control_info = None
self._extid = None
self._location = None
A ControlExtension is the service-side of a service-reference Feature. Where a ProtocolExtension extends a Device with protocol connectivity, a ControlExtension links a Service's control point to the Device that uses it.
Example: a power-control service has a ControlExtension per
device-port pair. The LPC934 power controller has 16
ports; 16 ControlExtensions, each linking to the device
plugged into that port. The Device's
PowerProtocolExtension holds a back-reference to the
service's ControlExtension; calls to device.power.cycle()
go through the protocol extension to the control extension to
the service.
Features
Per chapter 2, Features are declarations on a device in the Landscape. A Feature is configuration data, not a class.
- A boolean Feature (
isolation: true) is just a flag in the device's_featuresdict. - A service-reference Feature (
serial: serial-concentrator: 1) triggers the framework's Feature-attachment dispatcher to resolve the service, build an Agent, build a ProtocolExtension, and attach it to the device.
The dispatch is config-driven, not decorator-based. The
framework's LandscapeDevice (or its subclass) checks
if "serial" in self._features and acts. Adding a new
Feature name means subclassing LandscapeDevice and adding
a per-Feature attach step (chapter 2 worked an example).
The full chain — a worked traversal
A test calls device.serial.write("hello"). The chain:
- Test code holds a reference to a
LandscapeDevice. device.serialis theSerialProtocolExtensionattached at activation time by the framework's Feature dispatcher.- The ProtocolExtension's
write(data)method delegates to its wrapped Agent. - The Agent is a
SerialAgentinstance bound to the serial concentrator service at the device's declared port. - The Agent's
writemethod issues the protocol-level command — opening a TCP connection to the concentrator, selecting port 1, writing the bytes.
Going the other direction (the activation flow that built the chain):
- Coordinator activation — the integration layer's
per-deviceType Coordinator (e.g.,
SshCoordinatorfor the primary access) starts up. create_landscape_device— Coordinator instantiatesLandscapeDevicefrom the YAML config.- Primary ProtocolExtension — Coordinator wires the
device's primary protocol extension (
device.ssh). - Feature dispatcher — framework walks the device's
_featuresdict. Forserial, it resolves theserial-concentratorservice, builds aSerialAgent, wraps in aSerialProtocolExtension, attaches viadevice.attach_extension("serial", serial_ext). - Operational layer —
establish_connectivityverifies each ProtocolExtension actually connects.
Two sequences, two directions. The activation flow builds the chain at startup; the test-call flow walks the chain at runtime. Both use the same chain; the chain is the contract.
Disambiguating "extension"
The word "extension" appears in three places in the portfolio. Always disambiguate:
codecraftai-extensionis the namespace-package factory discovery package (Volume 0 chapter 7). Has nothing to do with Landscape.ProtocolExtensionis the device-level protocol binding (this chapter).ControlExtensionis the service-side control-point linkage (this chapter).BaseExt(chapter 8) is the service-specific framework-extension model (vSphere example).
When the book says "the extension," it's usually either ProtocolExtension or BaseExt. Context makes it clear; when in doubt, the chapter will disambiguate.
What the next chapter does
Chapter 4 walks FriendlyIdentifier — the framework's
mechanism for naming and resolving devices and services. The
class supports both regex-based identity matching and
substring-based equality with a non-trivial history (a
pass-4 audit fixed an __eq__ bug that always returned
True). The chapter walks the class's design and the
identity-vs-hint distinction with code anchors.