Chapter 21Lesson 05240–300 min

Checkpoint Lab — Dynamic and Hybrid Libraries, Remote Libraries, and Extension Architecture

Build equivalent static and dynamic providers, add a loopback Remote boundary, inject both a metadata mismatch and a connection failure, repair each layer, and produce an architecture decision record backed by evidence.

Checkpoint labStatic vs dynamicLoopback RemoteFailure injectionADR

Checkpoint objectives

  • Build equivalent static and dynamic keyword providers over one pure domain function.
  • Prove metadata through Libdoc and actual execution through Robot results.
  • Add a loopback-only Remote protocol boundary with explicit process ownership and cleanup.
  • Inject and diagnose one metadata mismatch and one Remote connection failure without erasing original evidence.
  • Produce an architecture decision record explaining when the added complexity is justified.

Current compatibility baseline — verified 2026-08-31. Robot Framework 7.4.2 is the stable course baseline and requires Python 3.8+. The mandatory examples use Robot Framework's current static, dynamic, hybrid, Libdoc, and built-in Remote interfaces. Dynamic libraries require get_keyword_names and run_keyword; metadata getters are optional but strongly recommended when the proxy knows signatures, types, tags, documentation, or source. Starting with Robot Framework 7.0, dynamic special methods may also be asynchronous, but this chapter keeps the learning path synchronous. Hybrid libraries perform dynamic name discovery but Robot calls Python methods directly. Remote is a dynamic proxy over XML-RPC and does not itself add authentication or transport security. The mandatory Remote exercise therefore binds a disposable protocol fixture only to 127.0.0.1, uses a bounded client timeout, and does not expose a remote-stop operation. The separately maintained robotremoteserver package is optional: its latest published 1.1.1 release explicitly documents Python support through 3.11, so it is not required for this course's Python 3.8+ local path.

1. Checkpoint scenario

You are reviewing an extension design proposed for a release pipeline. The same synthetic normalization operation is available through a static provider, a dynamic registry, and a process-isolated Remote provider. Your job is not merely to make all three pass: you must prove each contract, deliberately break two different layers, repair them minimally, and decide which architecture should be retained.

The entire checkpoint runs locally. The Remote fixture binds only to loopback, all identifiers are synthetic, and every generated file lives inside the lab/evidence directories.

2. Preflight and exact assumptions

python --version
python -m robot --version
python -m robot.libdoc --version
python -c "from robot.libraries.Remote import Remote; print(Remote.ROBOT_LIBRARY_SCOPE)"
  • Robot Framework baseline: 7.4.2 stable.
  • Python: 3.8+ supported by Robot 7.4.2; use one interpreter for all commands.
  • No external robotremoteserver dependency is required.
  • Network: only 127.0.0.1 ephemeral port.
  • Evidence directories are append/separate-by-run; never overwrite the injected-failure output.

3. Build the project

Create the same source files used in Lesson 2.

from __future__ import annotations


def normalize_token(token: str, prefix: str = "ID") -> str:
    """Pure Python domain function shared by every provider."""
    value = token.strip().upper().replace(" ", "-")
    if not value:
        raise ValueError("token must not be empty")
    if not value.replace("-", "").isalnum():
        raise ValueError(f"unsupported token characters: {token!r}")
    clean_prefix = prefix.strip().upper()
    if not clean_prefix.isalpha():
        raise ValueError("prefix must contain letters only")
    return f"{clean_prefix}:{value}"
from robot.api.deco import keyword, library

from .domain import normalize_token


@library(scope="TEST", version="1.0.0")
class StaticCatalog:
    """Known-at-import keyword surface: the default architecture."""

    @keyword("Normalize Token")
    def normalize_token_keyword(self, token: str, prefix: str = "ID") -> str:
        return normalize_token(token, prefix)

    @keyword("Describe Provider")
    def describe_provider(self) -> str:
        return "static"
from __future__ import annotations

from robot.api import logger

from .domain import normalize_token


class DynamicCatalog:
    """Runtime-discovered provider used only to demonstrate the dynamic API."""

    def get_keyword_names(self):
        return ["Normalize Token", "Describe Provider"]

    def get_keyword_arguments(self, name):
        specs = {
            "Normalize Token": ["token", "prefix=ID"],
            "Describe Provider": [],
        }
        return specs[name]

    def get_keyword_types(self, name):
        types = {
            "Normalize Token": {"token": "str", "prefix": "str"},
            "Describe Provider": {},
        }
        return types[name]

    def get_keyword_tags(self, name):
        return ["rf21", "dynamic"]

    def get_keyword_documentation(self, name):
        docs = {
            "__intro__": "Synthetic dynamic provider for Chapter 21.",
            "__init__": "No import arguments.",
            "Normalize Token": "Normalize a synthetic token and return prefix:value.",
            "Describe Provider": "Return the provider architecture name.",
        }
        return docs.get(name, "")

    def run_keyword(self, name, args, kwargs):
        logger.info(f"dynamic dispatch keyword={name}")
        if name == "Normalize Token":
            values = {"token": None, "prefix": "ID"}
            if len(args) > 2:
                raise AssertionError("Normalize Token accepts at most two positional arguments")
            for key, value in zip(("token", "prefix"), args):
                values[key] = value
            values.update(kwargs)
            return normalize_token(values["token"], values["prefix"])
        if name == "Describe Provider":
            return "dynamic"
        raise AssertionError(f"Unknown dynamic keyword: {name}")
from __future__ import annotations

import argparse
import signal
import threading
from pathlib import Path
from xmlrpc.server import SimpleXMLRPCServer


def normalize_token(token: str, prefix: str = "ID") -> str:
    value = token.strip().upper().replace(" ", "-")
    if not value:
        raise ValueError("token must not be empty")
    if not value.replace("-", "").isalnum():
        raise ValueError(f"unsupported token characters: {token!r}")
    clean_prefix = prefix.strip().upper()
    if not clean_prefix.isalpha():
        raise ValueError("prefix must contain letters only")
    return f"{clean_prefix}:{value}"


def get_keyword_names():
    return ["Normalize Token", "Describe Provider"]


def get_keyword_arguments(name):
    return {
        "Normalize Token": ["token", "prefix=ID"],
        "Describe Provider": [],
    }[name]


def get_keyword_types(name):
    return {
        "Normalize Token": {"token": "str", "prefix": "str"},
        "Describe Provider": {},
    }[name]


def get_keyword_tags(name):
    return ["rf21", "remote", "loopback"]


def get_keyword_documentation(name):
    return {
        "__intro__": "Loopback-only Chapter 21 Remote protocol fixture.",
        "__init__": "The server is started outside Robot and bound to 127.0.0.1.",
        "Normalize Token": "Normalize a synthetic token through XML-RPC.",
        "Describe Provider": "Return the remote provider architecture name.",
    }.get(name, "")


def run_keyword(name, args, kwargs=None):
    kwargs = kwargs or {}
    try:
        if name == "Normalize Token":
            values = {"token": None, "prefix": "ID"}
            for key, value in zip(("token", "prefix"), args):
                values[key] = value
            values.update(kwargs)
            result = normalize_token(values["token"], values["prefix"])
        elif name == "Describe Provider":
            result = "remote"
        else:
            raise ValueError(f"unknown remote keyword: {name}")
        return {
            "status": "PASS",
            "return": result,
            "output": f"*INFO* loopback remote executed {name}\n",
        }
    except Exception as exc:
        return {
            "status": "FAIL",
            "error": f"{type(exc).__name__}: {exc}",
            "traceback": "Synthetic fixture omits implementation traceback.",
        }


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--port-file", required=True)
    args = parser.parse_args()
    port_file = Path(args.port_file)
    port_file.parent.mkdir(parents=True, exist_ok=True)

    # SECURITY BOUNDARY — DO NOT USE 0.0.0.0 in this lab. Bind loopback only.
    server = SimpleXMLRPCServer(("127.0.0.1", 0), allow_none=False, logRequests=False)
    for func in (
        get_keyword_names,
        get_keyword_arguments,
        get_keyword_types,
        get_keyword_tags,
        get_keyword_documentation,
        run_keyword,
    ):
        server.register_function(func)

    port_file.write_text(str(server.server_address[1]), encoding="utf-8")
    print(f"RF21_REMOTE_READY http://127.0.0.1:{server.server_address[1]}", flush=True)

    def request_shutdown(*_):
        threading.Thread(target=server.shutdown, daemon=True).start()

    if hasattr(signal, "SIGTERM"):
        signal.signal(signal.SIGTERM, request_shutdown)
    signal.signal(signal.SIGINT, request_shutdown)
    try:
        server.serve_forever()
    finally:
        server.server_close()


if __name__ == "__main__":
    main()

Also create suites/local_providers.robot and suites/remote_provider.robot from Lesson 2. The hybrid provider is optional in this checkpoint because the required comparison is static versus dynamic, but keeping it provides useful third-party evidence.

4. Predict before executing

Action Prediction to record first
Generate static/dynamic Libdoc Both expose Normalize Token and Describe Provider; dynamic signature comes from metadata
Run local provider suite Static and dynamic return the same REF:AB-12 value
Start loopback server A new owned PID listens on 127.0.0.1 and writes one ephemeral port
Run Remote suite Remote result equals local contract and log includes server-provided INFO
Break dynamic metadata Two-argument call fails before dynamic dispatch
Stop server and rerun Remote test Connection/discovery fails while original PASS evidence remains unchanged

5. Establish the known-good local baseline

mkdir -p evidence/baseline/libdoc evidence/baseline/robot
python -m robot.libdoc -P src rf21_ext.static_catalog.StaticCatalog evidence/baseline/libdoc/static.html
python -m robot.libdoc -P src rf21_ext.dynamic_catalog.DynamicCatalog evidence/baseline/libdoc/dynamic.html
python -m robot --pythonpath src --outputdir evidence/baseline/robot suites/local_providers.robot

Do not continue until Libdoc and Robot agree about the two provider contracts. This is your pre-failure reference.

6. Establish the known-good Remote baseline

Bash / zsh

mkdir -p evidence/baseline/remote
rm -f evidence/baseline/remote/port.txt
python remote_server.py --port-file evidence/baseline/remote/port.txt > evidence/baseline/remote/server.log 2>&1 &
SERVER_PID=$!
for i in 1 2 3 4 5 6 7 8 9 10; do test -s evidence/baseline/remote/port.txt && break; sleep 0.2; done
PORT=$(cat evidence/baseline/remote/port.txt)
REMOTE_URI="http://127.0.0.1:${PORT}"
printf 'pid=%s\nuri=%s\n' "$SERVER_PID" "$REMOTE_URI" > evidence/baseline/remote/process.txt
python -m robot.libdoc "Remote::${REMOTE_URI}::5s" evidence/baseline/remote/libdoc.html
python -m robot --variable "REMOTE_URI:${REMOTE_URI}" --outputdir evidence/baseline/remote/robot suites/remote_provider.robot

PowerShell

New-Item -ItemType Directory -Force evidence/baseline/remote | Out-Null
$server = Start-Process python -ArgumentList 'remote_server.py','--port-file','evidence/baseline/remote/port.txt' -PassThru -RedirectStandardOutput evidence/baseline/remote/server.log -RedirectStandardError evidence/baseline/remote/server.err
for ($i=0; $i -lt 20 -and -not (Test-Path evidence/baseline/remote/port.txt); $i++) { Start-Sleep -Milliseconds 200 }
$port = (Get-Content evidence/baseline/remote/port.txt -Raw).Trim()
$remoteUri = "http://127.0.0.1:$port"
"pid=$($server.Id)`nuri=$remoteUri" | Set-Content evidence/baseline/remote/process.txt
python -m robot.libdoc "Remote::$remoteUri::5s" evidence/baseline/remote/libdoc.html
python -m robot --variable "REMOTE_URI:$remoteUri" --outputdir evidence/baseline/remote/robot suites/remote_provider.robot

Verify the server address literally starts with http://127.0.0.1:. Any other bind/URI fails the checkpoint safety requirement.

7. Failure injection A — metadata mismatch

Save a copy of the known-good dynamic provider, then change only its get_keyword_arguments result for Normalize Token from two supported arguments to ["token"].

# BROKEN line inside get_keyword_arguments:
"Normalize Token": ["token"],
mkdir -p evidence/failure-metadata/libdoc evidence/failure-metadata/robot
python -m robot.libdoc -P src rf21_ext.dynamic_catalog.DynamicCatalog evidence/failure-metadata/libdoc/dynamic.html
python -m robot --pythonpath src --outputdir evidence/failure-metadata/robot suites/local_providers.robot

Interpret the evidence: Libdoc advertises one argument; the Robot suite supplies two; Robot rejects the call before the valid domain implementation is reached. Preserve this directory unchanged.

8. Repair A — restore the public metadata contract

Restore ["token", "prefix=ID"], regenerate Libdoc, and rerun the local suite to a new directory:

mkdir -p evidence/repaired-metadata/libdoc evidence/repaired-metadata/robot
python -m robot.libdoc -P src rf21_ext.dynamic_catalog.DynamicCatalog evidence/repaired-metadata/libdoc/dynamic.html
python -m robot --pythonpath src --outputdir evidence/repaired-metadata/robot suites/local_providers.robot

The repair is accepted only when the metadata and execution evidence both return to the baseline contract.

9. Failure injection B — stop the owned Remote server

Stop the exact server PID recorded in the baseline. Keep the same URI value and run the Remote test into a new directory.

Bash

kill "$SERVER_PID"
wait "$SERVER_PID" 2>/dev/null || true
mkdir -p evidence/failure-connection
python -m robot --variable "REMOTE_URI:${REMOTE_URI}" --outputdir evidence/failure-connection suites/remote_provider.robot

PowerShell

Stop-Process -Id $server.Id -ErrorAction SilentlyContinue
$server.WaitForExit()
New-Item -ItemType Directory -Force evidence/failure-connection | Out-Null
python -m robot --variable "REMOTE_URI:$remoteUri" --outputdir evidence/failure-connection suites/remote_provider.robot

The expected failure must be attributable to the unavailable endpoint. Do not increase the timeout, edit the test to skip, or merge away this result.

10. Repair B — start a new owned loopback process

Start a fresh server. It may receive a different ephemeral port; that is expected. Record the new PID/URI, run Remote Libdoc again, then rerun only the Remote suite into evidence/repaired-connection. Finally stop the new process.

This proves that the fix was server availability/endpoint provenance, not a change to the domain keyword or assertion.

11. Serialization evidence

Use a direct XML-RPC call against the repaired server and retain the printed result dictionary:

python -c "from xmlrpc.client import ServerProxy; import sys; print(ServerProxy(sys.argv[1]).run_keyword('Normalize Token',['xy 9','LAB'],{}))" "$REMOTE_URI" \
  > evidence/serialized-result.txt

Expected shape contains plain scalar/dictionary values such as {'status': 'PASS', 'return': 'LAB:XY-9', ...}. Do not claim Python object identity survived the transport.

12. Architecture decision record

Complete this record after the experiments:

Chosen production shape:
Why static is or is not sufficient:
Why hybrid is or is not sufficient:
Why dynamic dispatch is or is not required:
Why a separate process/language boundary is or is not required:
Authoritative metadata source:
Serialization schema:
Network exposure/authentication design if Remote is retained:
Server lifecycle owner and health check:
Timeout contract:
Parallel-worker isolation:
Failure evidence retained:
Version compatibility test:
Simpler rollback architecture:

For this synthetic operation, the correct production answer is normally static; the dynamic and Remote forms exist to teach contracts, not because the domain requires them. A good ADR says that explicitly.

13. Required evidence packet

Artifact What it proves
Baseline static/dynamic Libdoc Known-good keyword metadata
Baseline local output.xml/log/report Equivalent in-process behavior
Baseline Remote Libdoc + output.xml/log/report Discovery and execution across XML-RPC
Loopback URI/PID/server-ready log Network/process ownership and private bind
Metadata-failure Libdoc/result Advertised signature mismatch caused pre-dispatch failure
Metadata-repair result Public metadata contract restored
Connection-failure result Endpoint unavailable while original PASS evidence preserved
Repaired-connection result Fresh server/URI restores behavior
Serialized result dictionary Wire-compatible data shape
ADR Why complexity is or is not justified

14. Verification checklist

  • Exactly five course lesson files are unrelated to the lab filesystem; the lab itself is disposable.
  • Static and dynamic providers call the same pure domain function.
  • Dynamic metadata includes names, arguments, types, tags, and documentation.
  • Hybrid, if retained, returns Python method names in the correct format.
  • Remote server binds only to 127.0.0.1 and uses an ephemeral port.
  • Remote client timeout is bounded.
  • No remote-stop capability, real credential, production endpoint, or public bind is required.
  • First-failure evidence directories are not overwritten by repaired runs.
  • Connection failure is repaired by restoring the owned service, not by giant retries/timeouts.
  • All owned server processes are stopped at the end.

15. Cleanup / rollback

Stop only the PIDs created by this lab. Restore the good dynamic metadata source. Keep evidence if it is part of the exercise submission; otherwise delete only the disposable rf21-extension-lab root. Do not kill unrelated Python processes or remove global environments.

If the server cannot be stopped cleanly, record the PID and failure, then terminate that exact owned process using the operating system. Cleanup failure is evidence—not a reason to hide the preceding Robot result.

16. What Chapter 21 adds—and the bridge to Chapter 22

Chapter 21 adds an extension-architecture decision framework to the Robot operating model: static by default; hybrid for justified runtime discovery with direct callables; dynamic for justified proxy/registry dispatch; Remote only when process/machine/language isolation pays for its serialization, service lifecycle, security, and observability costs.

Chapter 22 moves from providing keywords to observing and transforming Robot execution itself with listeners, pre-run modifiers, visitors, and public programmatic APIs. The same principle continues: use explicit public contracts, preserve evidence, and add extension power only when the operational requirement warrants it.

Knowledge check

Why is the metadata mismatch a different failure layer from the Remote outage?

What evidence proves the Remote endpoint stayed local?

Why is a passing repaired Remote run insufficient if the original failure directory was overwritten?

For the synthetic normalization operation, which architecture should the ADR normally select?

What new subject does Chapter 22 add beyond these library APIs?

Next lesson

Listeners, PreRunModifiers, Visitors, and Programmatic Execution: Core Concepts and Mental Model

Continue with Listeners, PreRunModifiers, Visitors, and Programmatic Execution: Core Concepts and Mental Model. It builds directly on the state, evidence, and operating assumptions established here, so carry those constraints forward rather than treating the next page as an isolated topic.

References and version anchors

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.