Volume 6 — Topology, Provisioning, and UI Automation — contents
  1. Chapter 1 — Landscape — Physical Inventory and Wiring
  2. Chapter 2 — Inside a Device Declaration
  3. Chapter 3 — Coordinators, Agents, ProtocolExtensions, Features
  4. Chapter 4 — Device Discovery and Friendly Identifiers
  5. Chapter 5 — Topology — Logical Organisation and Roles, with Verification
  6. Chapter 6 — Topology as an Executable Assertion
  7. Chapter 7 — Subclassing Landscape and Topology — The Customisation Mechanism
  8. Chapter 8 — Service-Specific Framework Extensions
  9. Chapter 9 — The Pod Model — Landscape + Topology Realised
  10. Chapter 10 — Cloud Provisioning Model
  11. Chapter 11 — Cross-Platform UI Automation
  12. Chapter 12 — Declarative UI Mappings
  13. Chapter 13 — GitLab Integration
  14. Chapter 14 — Web-Service Scaffolding with Flask
  15. Chapter 15 — Case Study: A Landscape That Drives a Sysman Feature Test Across Four OSes

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:

  • SshCoordinator at coordinators/ssh/sshcoordinator.py:53. Handles network/ssh deviceTypes. Its establish_connectivity (line 137–156) executes a remote echo command via device.ssh.run_cmd() and collects results.
  • SerialCoordinatorBase at coordinators/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 _extensions dict at line 113 of landscapedevice.py carries 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:

  • ProtocolExtension extends a LandscapeDevice with protocol connectivity. The most common kind of extension.
  • BaseExt is 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 _features dict.
  • 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:

  1. Test code holds a reference to a LandscapeDevice.
  2. device.serial is the SerialProtocolExtension attached at activation time by the framework's Feature dispatcher.
  3. The ProtocolExtension's write(data) method delegates to its wrapped Agent.
  4. The Agent is a SerialAgent instance bound to the serial concentrator service at the device's declared port.
  5. The Agent's write method 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):

  1. Coordinator activation — the integration layer's per-deviceType Coordinator (e.g., SshCoordinator for the primary access) starts up.
  2. create_landscape_device — Coordinator instantiates LandscapeDevice from the YAML config.
  3. Primary ProtocolExtension — Coordinator wires the device's primary protocol extension (device.ssh).
  4. Feature dispatcher — framework walks the device's _features dict. For serial, it resolves the serial-concentrator service, builds a SerialAgent, wraps in a SerialProtocolExtension, attaches via device.attach_extension("serial", serial_ext).
  5. Operational layer — establish_connectivity verifies 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-extension is the namespace-package factory discovery package (Volume 0 chapter 7). Has nothing to do with Landscape.
  • ProtocolExtension is the device-level protocol binding (this chapter).
  • ControlExtension is 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.