Chapter 9 — Spec-Driven Development for System Administration
Eight chapters walked the contracts and the implementations. This chapter walks the discipline that keeps them aligned: the 11 Implementation Conventions documented at
codecraftai-sysman-common/specs/contracts/README.md:75–364. The conventions are what prevent drift across 5 OSes × ~50 features without a code generator.
The volume's previous chapters referenced the conventions piecemeal — Convention 4 (OS-prefixed names), Convention 5 (FAILED with stable token), Convention 11 (Homebrew elevation inversion). This chapter walks all eleven as a unified discipline. By the end, a reader writing a new per-OS implementation knows the rules in detail.
The conventions emerged from real implementation work — specifically the Phase-3 cross-OS push that brought Ubuntu, macOS, and Windows online alongside the original RHEL package. Each convention is a crystallisation of a problem the implementation work uncovered.
Why conventions instead of code
A reader from a heavily-tooled engineering culture might ask: if these are uniform rules across all packages, why not enforce them in code via metaclasses, decorators, or a linter?
Three reasons:
Many of the conventions are naming
Convention 4 (OS-prefixed type names) is enforceable by a
linter. But the discipline isn't just the prefix — it's
that the prefix appears consistently across types
(UbuntuPackageManager, UbuntuPackageActionResult,
UbuntuPackageActionStatus). A linter that just checks
class-name prefixes misses the cross-type alignment.
Many of the conventions are trade-offs
Convention 7 (reads degrade gracefully — return None / empty / synthetic record vs. raise) gives implementations choice. The discipline is that the implementation picks one and is consistent. Code can't enforce "be consistent in your choice."
The conventions evolve
Phase-3 added Convention 6 (elevation_blocked). A future phase might add Convention 12. Hard-coding the conventions in framework code makes them harder to evolve; documenting them in prose lets them grow.
The eleven conventions
Convention 1 — ICommandRunner is the file-I/O seam
File I/O MAY use
IFileOperatorif the host package provides one; the established pattern is to route file I/O throughICommandRunnerusing OS-native file utilities.
The framework has no separate file-operator abstraction.
Reading, writing, removing files all goes through the same
ICommandRunner that runs commands. On Linux, that means
invoking cat / tee / rm / mv. On Windows, it means
invoking Get-Content / Set-Content / Remove-Item.
The convention exists because:
- A separate
IFileOperatorwould duplicate concerns the runner already handles (elevation, dry-run, audit recording). - Routing file I/O through the runner means the audit trail (every action records its argv per Convention 2) works for file operations too.
- Tests that mock the runner automatically also mock the file operations.
A worked example. When a feature needs to write a config file:
# Wrong — bypasses the runner.
with open("/etc/myservice.conf", "w") as f:
f.write(content)
# Right — routes through the runner per Convention 1.
self._runner.run(
["tee", "/etc/myservice.conf"],
elevated=True,
stdin=content,
)
The runner-routed version:
- Goes through the manager's elevation logic.
- Captures the argv in the action result.
- Honours dry-run (the runner doesn't invoke when
dry_run=True). - Routes through SSH when the runner is an
SshAgent(so the file write happens on the remote host, not the controller's filesystem).
Convention 2 — command: list[str] | None field on action results
Every per-OS
*ActionResultdataclass carries acommandfield that records the argv that was (or would have been) executed.
The command field on every result enables:
- Audit logging — what changed and how.
- Dry-run inspection — see the would-have-been argv.
- Test verification — assert on the constructed command.
- Forensic debugging — when a test fails, the command field tells you exactly what ran.
featurebase._dispatch populates this automatically. A
manager that doesn't use _dispatch (chapter 4's
PackageManager example) has to populate it manually:
result = PackageActionResult(
status=PackageActionStatus.SUCCESS,
package_name=name,
version=installed_version,
command=command_copy, # Convention 2
exit_code=run_result.exit_code,
stdout=run_result.stdout,
stderr=run_result.stderr,
)
The field is Optional[list[str]] because SKIPPED
results don't have a command (no command was needed).
Convention 3 — requires_reboot: bool on action results that mutate boot-time-only state
Features that mutate boot-time-only state (Windows hostname rename, UEFI capsule-update) carry a
requires_reboot: bool = Falsefield set toTruewhen the underlying tool reports the change was staged but a reboot is required.
Some operations succeed in the sense that the tool accepted the change, but the change doesn't take effect until reboot. The result needs to carry both signals: "yes, this succeeded" and "but the system needs to reboot."
Worked example: Windows hostname rename.
# WindowsHostnameManager.rename_computer
def rename_computer(self, new_name: str) -> WindowsHostnameActionResult:
# ... build PowerShell Rename-Computer argv ...
result = self._dispatch(
command=ps_argv,
new_name=new_name,
)
# Rename-Computer reports success but the new name
# isn't visible until reboot.
if result.status == WindowsHostnameActionStatus.SUCCESS:
result.requires_reboot = True
return result
The test calling rename_computer checks
result.requires_reboot and either schedules the reboot or
flags the test as needing a manual restart.
Convention 4 — OS-prefixed concrete type names
Already covered extensively (chapters 4 and 5). Recap:
- Per-OS implementations prefix every public type:
UbuntuPackageManager,MacosFirewallManager,WindowsServiceManager. - RHEL retains unprefixed names (
PackageManager, etc.) for historical reasons; this is the documented exception. - The prefix applies to managers, configs, results, statuses, info classes — every public type the package exposes.
Convention 5 — FAILED with a stable detail token for unsupported (host_os, verb) combos
When a feature has no scriptable implementation on a given host OS, the manager returns
*ActionResultwithstatus=FAILEDanddetailset to a machine-readable token likeunsupported_on_host_os_darwin.
This replaces the older "raise NotImplementedError"
pattern. The convention's value:
- The contract verb is reachable from any host.
- The result is machine-discoverable — test code checks
result.detailfor known tokens. - The error path is uniform with other failures —
status=FAILEDwith a structureddetail.
Stable tokens documented in the contracts. Examples:
| Token | Meaning |
|---|---|
unsupported_on_host_os_darwin |
The feature has no macOS implementation. |
unsupported_on_host_os_windows |
No Windows implementation. |
ufw_no_zones |
ufw doesn't support multiple zones. |
unsupported_no_mok_concept_macos |
macOS has no MOK equivalent. |
unsupported_runtime_change_macos |
The change requires recovery mode. |
A test checking for any unsupported case can match the
unsupported_* prefix. Tests checking for a specific
case match the full token.
Convention 6 — elevation_blocked: bool on Windows RunResult
Already covered in chapter 7. Recap:
- Windows can't drive UAC non-interactively.
- The Windows runner pre-detects elevation status.
- When a manager requests
elevated=Trueand the calling process isn't elevated, the runner returns aRunResultwithelevation_blocked=True. - Manager code propagates the flag onto the
*ActionResult. - Test code checks
result.elevation_blockedand skips with a clear reason.
Convention 7 — Reads degrade gracefully
Read methods MAY raise
*QueryErrorOR return a None / empty / synthetic record. Whichever the per-OS spec picks, it MUST be consistent and documented.
The convention gives implementations flexibility on read-failure semantics but requires consistency within an implementation.
Two acceptable patterns:
Raise-on-error
def get_package_info(self, name: str) -> PackageInfo:
"""
Returns PackageInfo for the named package.
:raises PackageQueryError: if the package isn't installed.
"""
info = self._query(name)
if info is None:
raise PackageQueryError(f"Package '{name}' is not installed")
return info
Return-None-on-not-found
def get_package_info(self, name: str) -> Optional[PackageInfo]:
"""
Returns PackageInfo for the named package, or None if
not installed.
"""
info = self._query(name)
return info # may be None
The discipline is that all reads in a single implementation pick the same pattern. A package whose some reads raise and some return None is broken.
The per-OS spec documents which pattern the package uses. Test code is written against the documented choice.
Convention 8 — Per-OS backend enums
Features ship a single abstract concept on the contract (e.g.,
BackupKind,VirtBackend) that diverges into OS-specific enums in practice.
Some features have an abstract concept (a backup kind, a virtualisation backend) that has different concrete options per OS. The contract defines the abstract concept; the per-OS implementation defines its own enum.
Worked example: virtualization-management ships with a
VirtBackend abstract enum on the contract, but each per-OS
implementation has its own:
class UbuntuVirtBackend(Enum):
LIBVIRT_KVM = "libvirt-kvm"
LIBVIRT_LXC = "libvirt-lxc"
LXC = "lxc"
QEMU = "qemu"
class MacosVirtBackend(Enum):
HYPERVISOR_FRAMEWORK = "hypervisor-framework"
VIRTUALIZATION_FRAMEWORK = "virtualization-framework"
PARALLELS = "parallels"
VMWARE_FUSION = "vmware-fusion"
class WindowsVirtBackend(Enum):
HYPER_V = "hyper-v"
VMWARE_WORKSTATION = "vmware-workstation"
VIRTUALBOX = "virtualbox"
Cross-OS test code that needs to handle the
VirtBackend argument has per-OS dispatch:
if isinstance(backend, UbuntuVirtBackend):
# Ubuntu-specific handling
...
elif isinstance(backend, MacosVirtBackend):
# macOS-specific handling
...
The convention acknowledges that some abstractions don't collapse cleanly and provides a structured way to handle them.
Convention 9 — Concrete-implementation naming overrides
The contract uses a clean abstract name (
SystemIdentity,MailTransportConfig) while the per-OS implementation uses a name that fits its package (UbuntuHostIdentity,UbuntuMailTransportConfig).
Convention 4 prefixes the implementation types. Convention 9 covers the case where the contract type name doesn't fit the per-OS package's terminology.
Worked example: the contract has a type called
SystemIdentity describing the host's identity profile.
The Ubuntu implementation calls its corresponding type
UbuntuHostIdentity because Ubuntu's terminology calls the
machine a host, not a system.
The convention allows this divergence with cross-OS
adapter layers translating at the boundary. A test that
needs to use the contract's SystemIdentity against
multiple OSes either:
- Writes per-OS code that uses each OS's preferred name.
- Uses an adapter function that converts between the contract type and the implementation type.
Convention 10 — UEFI managers carry override_host_os
Already covered in chapter 8. Recap:
- UEFI managers detect host OS at construction.
- The
override_host_os: str | None = Noneconfig field forces a specific dispatch path for testing. - This is testability infrastructure unique to UEFI's single-tier dispatch architecture.
Convention 11 — Elevation-model inversion
Already covered in chapters 6 and 9. Recap:
- Some tools (Homebrew on macOS) refuse to run as root.
- The manager passes
elevated=Falseunconditionally. - When the calling process is root, the manager wraps
the argv with
sudo -u <non_root_user>. - The pattern is documented per-feature in the Implementation Drift section of the contract.
Credential-delivery conventions
In addition to the eleven implementation conventions, the contracts document credential-delivery conventions at lines 299–328 of the README.
The three patterns, in order of preference:
1. Stdin (preferred)
result = self._dispatch(
command=["luksFormat", device_path],
stdin=passphrase, # secret stays out of argv
detail=device_path,
)
Secrets flow through stdin. The RunResult doesn't carry
the stdin data, so the command field never contains the
secret. The audit trail records the argv (["luksFormat", "/dev/sdb"]) without leaking the passphrase.
2. In-process secure-string (Windows)
$securePassword = ConvertTo-SecureString -String $env:PWD -AsPlainText -Force
Set-LocalUser -Name $username -Password $securePassword
PowerShell has SecureString — an in-memory representation that's not paged to disk and not visible in the process listing. Windows managers route credentials through SecureString when the underlying cmdlet supports it.
3. Argv with redaction (last resort)
# Some tools accept credentials only on argv.
command = ["smbpasswd", "-a", username, "-p", password]
result = self._dispatch(
command=command,
detail=username,
redact_argv_indices=[4], # mark the password slot for redaction
)
When a tool only accepts credentials on argv, the manager
marks the secret-bearing positions so the action
result's command field has the secret redacted before
recording. The framework's argv-redaction logic replaces
the marked slots with "[REDACTED]" in the recorded
command.
The convention prefers stdin → SecureString → argv-with- redaction. Argv-with-redaction is acknowledged as the fallback for tools that won't accept the better patterns.
How the conventions are enforced
The conventions aren't enforced by tooling. They're enforced by:
Code review
Every PR that touches a sysman package goes through review. Reviewers cross-reference the change against the conventions document. A convention violation is a review comment.
Per-OS test coverage
The contract's "Tests" section prescribes minimum tests. Tests catch convention violations indirectly:
- A manager that doesn't populate
command(Convention 2) fails its dry-run test (the test assertsresult.command == [...]). - A manager that doesn't honour
dry_run(Convention 1's scope) fails its dry-run test. - A manager that elevates reads (against the contract) fails its elevation test.
Tests catch the symptoms of convention violations even when the convention isn't tested directly.
Cross-OS comparisons
When two OSes implement the same feature, comparing their
tests catches drift. An RHEL PackageManager.install test
that's structurally different from a UbuntuPackageManager.install
test is a sign one of them is wrong.
What the conventions give you
A reader who understands the eleven conventions can:
- Read any sysman feature manager and know what to expect.
- Add a new feature to an existing OS by following the conventions.
- Add a new OS implementation by following the conventions
- the contracts.
- Spot convention violations during code review.
- Write tests that exercise the convention's behaviour.
The conventions are the discipline. The framework's 50 contracts × 5 OSes = ~250 implementations stay aligned because the conventions are documented, reviewed, and tested.
What's next
Chapter 10 closes the volume by walking cross-OS test parity — how the suite achieves consistent green-across-OSes test runs at the scale of thousands of tests. The chapter explains how the contracts + featurebase + 11 conventions all compose to make a multi-OS test campaign tractable.