diff --git a/crates/sysknife-brain/src/planning_tools/propose_plan.rs b/crates/sysknife-brain/src/planning_tools/propose_plan.rs index a5859f20..0c1d8497 100644 --- a/crates/sysknife-brain/src/planning_tools/propose_plan.rs +++ b/crates/sysknife-brain/src/planning_tools/propose_plan.rs @@ -479,6 +479,11 @@ reports live interface state"), fn available_on(action: &str, hint: Option<&sysknife_types::DistroHint>) -> bool { let family = hint.map(|hint| hint.family); if let Some(hint) = hint { + // Ubuntu Core is immutable and is not an eligible Debian host. Do not + // offer apt (or other host-policy actions) merely from its family tag. + if hint.id == "ubuntu-core" { + return !action_requires_supported_host(action); + } if hint.id != "ubuntu" && UBUNTU_ONLY_ACTIONS.contains(&action) { return false; } @@ -735,6 +740,20 @@ mod tests { #[test] fn debian_host_does_not_inherit_ubuntu_tools_or_preferences() { + let core = sysknife_types::DistroHint { + id: "ubuntu-core".into(), + family: DISTRO_FAMILY_DEBIAN, + version: Some("24".into()), + }; + let core_def = propose_plan_tool_def(Some(&core)); + let core_actions = offered_actions(&core_def); + for (action, _) in KNOWN_ACTIONS { + assert_eq!( + core_actions.contains(&action.to_string()), + !action_requires_supported_host(action), + "Ubuntu Core: {action}" + ); + } let hint = sysknife_types::DistroHint { id: "debian".into(), family: DISTRO_FAMILY_DEBIAN, diff --git a/crates/sysknife-daemon/src/dispatcher.rs b/crates/sysknife-daemon/src/dispatcher.rs index 14a4ea06..9cd37dc7 100644 --- a/crates/sysknife-daemon/src/dispatcher.rs +++ b/crates/sysknife-daemon/src/dispatcher.rs @@ -5079,7 +5079,8 @@ mod tests { // `AptUpdate` is RiskLevel::Low, so min_role_for_action puts it at // Observer — yet it runs `sudo apt-get update`. Any exemption keyed on // the RBAC role therefore lets a privileged mutation through, which is - // why the platform gate does not exempt reads at all. + // why the platform gate does not exempt hard-fenced reads. Portable + // Observer reads still return early without requiring distro detection. let dir = tempdir().unwrap(); let mut state = test_state(&dir); state.host_distro = Some(sysknife_core::distro::DistroId::Debian { version: Some(11) }); diff --git a/docs/action-compatibility.md b/docs/action-compatibility.md index 15f9a968..f7ef78f5 100644 --- a/docs/action-compatibility.md +++ b/docs/action-compatibility.md @@ -27,6 +27,11 @@ longer require distro detection after leaving the hard lists. This widens read-only inspection; mutations still require an eligible host, and the Low-risk mutating `AptUpdate` remains hard-fenced. +Ubuntu Core is not an eligible host. Its Debian family tag does not make apt +available: the planner limits its catalogue to actions outside the shared +host-policy predicate, as it does for an unknown family. This does not claim +Ubuntu Core execution support. + The MCP surface uses the same routing checks and withholds hard-restricted actions when detection fails. The planner receives an explicit distribution ID alongside the family; display text never grants Ubuntu capabilities.