H7: Add role:* capability slots — catch missing IIS/Docker/nginx at plan-time
Summary
H7 of the 1.8.0 Upgrade Hardening initiative (H1-H6 already merged).
Closes the gap from the operator's actual production scenario where IIS deploy dispatched to a Windows machine without IIS installed, ran the script all the way to Import-Module WebAdministration, and ONLY THEN failed with "module not found". H7 lets handlers declare role requirements that CapabilityValidator catches at plan-time — before any wasted dispatch.
What's new in the taxonomy
public static class CapabilityKeys.Role // H7
{
public const string IIS = "role:iis"; // Windows W3SVC
public const string Docker = "role:docker"; // Docker daemon
public const string Nginx = "role:nginx"; // nginx service (Linux)
public const string Systemd = "role:systemd"; // systemd init (Linux)
}
IISDeployActionHandler.StaticRequirements now declares os:windows + shell:powershell + role:iis.
Agent-side detection
RuntimeCapabilitiesInspector.DetectInstalledRoles() probes cheaply (~50ms per role) and reports comma-separated in the new installedRoles metadata key:
| Probe | Tool |
|---|---|
iis (Windows) |
sc.exe query W3SVC exit 0 |
docker (Windows) |
docker on PATH + com.docker.service registered |
docker (Linux) |
docker on PATH + systemctl status docker.service exit 0/3 |
nginx (Linux) |
systemctl status nginx.service exit 0/3 |
systemd (Linux) |
systemctl on PATH |
Probe failures are silent — operator just doesn't see that role advertised; handler requirements fall to optimistic-allow.
Wire path
Agent: RuntimeCapabilitiesInspector → metadata["installedRoles"] = "iis,docker"
↓ (Halibut CapabilitiesResponse)
Server: TentacleHealthCheckStrategy.CacheCapabilitiesFor
↓
InMemoryMachineRuntimeCapabilitiesCache.InstalledRoles
↓ (H2 — also persisted to machine.runtime_capabilities_json)
↓
DeploymentPlanner → MachineCapabilitySet.From(caps)
↓
projects "iis" into role:iis slot with value Present
↓
CapabilityValidator → IISDeployActionHandler.StaticRequirements
requires role:iis
MATCH SUCCESS or
FAIL with "Missing capability: role:iis" + hint
Backward compatibility (no big-bang migration)
-
Pre-H7 agents don't emit
installedRolesmetadata → cache has emptyInstalledRoles→ProjectRolesproduces norole:*slots → CapabilityValidator's "absent slot = unknown = optimistic-allow" path → existing fleets continue to plan/dispatch IIS deploy normally. - Strict validation activates only after the operator upgrades the agent to a version that detects + advertises roles. Per-machine, gradual.
-
H2 persistence DTO has
InstalledRolesas nullable — blobs written by pre-H7 servers deserialise cleanly withnull → string.Emptycoalesce.
Test plan
-
+4 projection tests (Theory × 4 single-role + 1 multi-role + 1 empty-roles backward-compat + 1 whitespace/dupe normalisation) -
DeploymentPlannerStaticRequirementsTests(PR #347/#348) already exercises the validator loop; H7'srole:iisrequirement plugs in without test changes -
5585/5585 unit tests green (+7 net new) -
Integration: agent role probe behaviour deferred to H8 (E2E matrix includes "deploy IIS to machine without IIS installed → plan-time error" scenario)
Operator UX after H7
When operator tries to deploy IIS to a machine that lacks IIS:
Release validation failed for step 'Deploy to IIS':
Machine 'WEB-01' is missing required capability: role:iis.
Install IIS via Server Manager → Add Roles and Features → Web Server (IIS),
then run an active health check (POST /api/machines/{id}/health-check)
to repopulate the capability cache. Retry the release once role:iis appears
in the machine's capability snapshot.
(Exact message format depends on CapabilityValidator.ValidateStaticRequirements's existing error renderer.)
What's NOT in this PR
- H8: Comprehensive E2E test matrix (final — exercises the full upgrade + deploy chain end-to-end)