Add handler-level static capability requirements (slot-based, AND/OR)
Summary
- New property
IActionHandler.StaticRequirements(default empty) lets handlers declare plan-time capability requirements as a map ofslot → acceptable values. Match semantics: AND across slots (every declared slot must be satisfied), OR within a slot (handler accepts any value in the acceptable-values set). - The single mechanism handles OS, shell, binary, privilege, architecture, and any future dimension expressible as namespaced strings — no interface changes needed when a new capability category appears.
-
CapabilityValidator.ValidateStaticRequirementsis the new validator dimension. The planner pulls the handler from the registry, projects the target'sMachineRuntimeCapabilitiesinto the slot-map shape viaMachineCapabilitySet, and matches slot-by-slot. Violations flow through the existingCapabilityViolationpipeline →PlanBlockingReasonCodes.CapabilityViolation→ preview UI. -
IISDeployActionHandlerdeclares{os: {windows}, shell:powershell: {present}}.RunScriptActionHandlerdeclares{os: {windows, linux, macos}}— the explicit OR-within-slot example. Existing dispatch-time per-handler guards stay in place as the runtime safety net.
Design choices
Slot-based, not boolean-per-dimension — IReadOnlyDictionary<string, IReadOnlySet<string>> keeps the surface generic. Adding a new capability dimension (e.g. min agent version, network egress) doesn't require interface changes; just add a new namespaced slot (agent-min-version:1.7.0, net:outbound-https).
AND across slots, OR within a slot — covers the realistic distribution of action requirements: IIS deploy needs Windows AND PowerShell (two slots); RunScript needs Windows OR Linux OR macOS (one slot, three values); Helm needs Linux OR macOS AND bin:helm AND bin:kubectl (three slots, one with two values).
Strict slot semantics at plan time — a slot the handler requires that the target hasn't advertised IS a violation. The message tells the operator to run a health check on the target. The dispatch-time per-handler guard (e.g. IISDeployActionHandler.EnsureWindowsTentacleTarget) keeps the optimistic-allow safety net for cache-went-stale-between-preview-and-execute.
OS-string tolerance in projection — MachineCapabilitySet.From normalises both the canonical "Windows" (current Tentacle) AND the legacy long form "Microsoft Windows NT 10.0.19045.0" (older binaries) into os: windows. Anchored on StartsWith("Microsoft Windows"), not Contains("Windows"), with explicit anti-false-positive tests (LinuxOnWindowsSubsystem must NOT match).
Test plan
-
dotnet build Squid.sln→ 0 errors -
dotnet test --filter "Capability"(new tests) → 66/66 pass, 186ms -
dotnet test(full Squid.UnitTests) → 5410/5410 pass, zero regression -
CI: unit + integration tests on the workflow -
Manual smoke (post-merge): create release for an IIS step against a target whose cache reports a non-Windows OS — preview should now show a CapabilityViolationwith the slot-mismatch detail BEFORE the operator clicks Deploy
Future capability dimensions (no interface changes needed)
The same mechanism naturally extends to:
-
Binary presence:
bin:kubectl,bin:helm,bin:docker— Helm action declaresbin:helm: {present} -
Privilege:
priv:admin,priv:sudo—WindowsServiceInstallaction declarespriv:admin: {present} -
Architecture:
arch:x64,arch:arm64— handlers needing native binaries declare matching arch - Comparison-based (e.g. min agent version) would need a separate dimension since flat strings can't express ordering; YAGNI for now
Backward compatibility
-
IActionHandler.StaticRequirementsdefaults to empty → every existing handler keeps its current behaviour -
CapabilityValidator.Validate(existing 5-dimension method) is unchanged → existing planner check paths untouched -
DeploymentPlannerconstructor adds one new dependency (IMachineRuntimeCapabilitiesCache); existing planner test fixtures updated with a permissive default