clawft_kernel/workload_governance/
policy.rs1use serde::{Deserialize, Serialize};
5
6use super::effect::{NetworkPolicy, NodeTrustTier, PackageTrust, WorkloadEffect};
7use crate::governance::{
8 GovernanceBranch, GovernanceRule, GovernanceRuleType, RuleSeverity, selector_matches,
9};
10
11pub const WORKLOAD_ACTION_PREFIX: &str = "workload.";
13
14pub const ACTION_INSTALL: &str = "workload.install";
16pub const ACTION_PLACE: &str = "workload.place";
18pub const ACTION_LOAD: &str = "workload.load";
20pub const ACTION_START: &str = "workload.start";
22pub const ACTION_STOP: &str = "workload.stop";
24pub const ACTION_UNLOAD: &str = "workload.unload";
26pub const ACTION_MIGRATE: &str = "workload.migrate";
28pub const ACTION_REVOKE: &str = "workload.revoke";
30pub const ACTION_NODE_BIND: &str = "workload.node.bind";
32
33pub const GOVERNED_ACTIONS: &[&str] = &[
36 ACTION_INSTALL,
37 ACTION_PLACE,
38 ACTION_LOAD,
39 ACTION_START,
40 ACTION_STOP,
41 ACTION_UNLOAD,
42 ACTION_MIGRATE,
43 ACTION_REVOKE,
44 ACTION_NODE_BIND,
45];
46
47pub const DEFAULT_DENY_RULE_ID: &str = "WORKLOAD-DEFAULT-DENY";
49pub const EFFECT_CEILING_RULE_ID: &str = "WORKLOAD-EFFECT-CEILING";
51pub const DEFAULT_DENY_CATEGORY: &str = "workload.default_deny";
53
54pub fn is_governed_action(action: &str) -> bool {
56 GOVERNED_ACTIONS.contains(&action)
57}
58
59pub fn default_rules() -> Vec<GovernanceRule> {
70 let rule = |id: &str, desc: &str, category: &str, force: bool| GovernanceRule {
71 id: id.into(),
72 description: desc.into(),
73 branch: GovernanceBranch::Legislative,
74 severity: RuleSeverity::Blocking,
75 active: true,
76 reference_url: None,
77 sop_category: Some(category.into()),
78 rule_type: GovernanceRuleType::General,
79 action_selector: Some(format!("{WORKLOAD_ACTION_PREFIX}*")),
80 tool_selector: None,
81 force_on_match: force,
82 };
83 vec![
84 rule(
85 DEFAULT_DENY_RULE_ID,
86 "workload.* actions are denied unless an explicit workload permit rule matches (ADR-099 s4)",
87 DEFAULT_DENY_CATEGORY,
88 true,
89 ),
90 rule(
91 EFFECT_CEILING_RULE_ID,
92 "workload.* actions above the effect threshold are denied even when permitted",
93 "workload.effect_ceiling",
94 false,
95 ),
96 ]
97}
98
99pub fn install_default_rules(
101 dist: &mut crate::rule_distribution::RuleDistribution,
102 now_unix: u64,
103) {
104 for rule in default_rules() {
105 dist.upsert_local(rule, now_unix);
106 }
107}
108
109fn default_min_trust() -> PackageTrust {
110 PackageTrust::PinnedSigner
111}
112fn default_min_tier() -> NodeTrustTier {
113 NodeTrustTier::Paired
114}
115fn default_max_network() -> NetworkPolicy {
116 NetworkPolicy::Lan
117}
118fn default_max_cost() -> f64 {
119 1.0
120}
121
122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128pub struct WorkloadPermitRule {
129 pub id: String,
131 #[serde(default)]
133 pub description: String,
134 pub actions: Vec<String>,
136 pub kinds: Vec<String>,
138 #[serde(default = "default_min_trust")]
140 pub min_package_trust: PackageTrust,
141 #[serde(default = "default_min_tier")]
143 pub min_node_tier: NodeTrustTier,
144 #[serde(default = "default_max_network")]
146 pub max_network: NetworkPolicy,
147 #[serde(default)]
149 pub allow_secrets: bool,
150 #[serde(default)]
152 pub allow_emulated: bool,
153 #[serde(default)]
155 pub accelerators: Vec<String>,
156 #[serde(default = "default_max_cost")]
158 pub max_resource_cost: f64,
159 #[serde(default, skip_serializing_if = "Vec::is_empty")]
164 pub projects: Vec<String>,
165 #[serde(default, skip_serializing_if = "Vec::is_empty")]
170 pub principals: Vec<String>,
171}
172
173pub const CATALOG_PRINCIPAL: &str = "catalog";
179
180pub const SUPERVISOR_PRINCIPAL: &str = "weftos.project-supervisor";
185
186pub fn project_supervisor_permit() -> WorkloadPermitRule {
189 let mut rule = WorkloadPermitRule::new(
190 "PROJECT-SUPERVISOR-PERMIT",
191 [ACTION_LOAD, ACTION_START, ACTION_STOP, ACTION_UNLOAD],
192 ["project"],
193 );
194 rule.description = "the project supervisor may run per-project child kernels (ADR-103 A6)".into();
195 rule.min_package_trust = PackageTrust::ProjectCert;
196 rule.min_node_tier = NodeTrustTier::Paired;
197 rule.max_network = NetworkPolicy::None;
198 rule.principals = vec![SUPERVISOR_PRINCIPAL.into()];
199 rule
200}
201
202impl WorkloadPermitRule {
203 pub fn new(
205 id: impl Into<String>,
206 actions: impl IntoIterator<Item = impl Into<String>>,
207 kinds: impl IntoIterator<Item = impl Into<String>>,
208 ) -> Self {
209 Self {
210 id: id.into(),
211 description: String::new(),
212 actions: actions.into_iter().map(Into::into).collect(),
213 kinds: kinds.into_iter().map(Into::into).collect(),
214 min_package_trust: default_min_trust(),
215 min_node_tier: default_min_tier(),
216 max_network: default_max_network(),
217 allow_secrets: false,
218 allow_emulated: false,
219 accelerators: Vec::new(),
220 max_resource_cost: default_max_cost(),
221 projects: Vec::new(),
222 principals: Vec::new(),
223 }
224 }
225
226 pub fn validate(&self) -> Result<(), String> {
228 if self.id.trim().is_empty() {
229 return Err("permit rule id must not be empty".into());
230 }
231 if self.actions.is_empty() || self.kinds.is_empty() {
232 return Err(format!("permit rule '{}' needs at least one action and one kind", self.id));
233 }
234 for a in &self.actions {
235 let known = is_governed_action(a)
236 || a == "workload.*"
237 || (a.ends_with('*') && a.starts_with(WORKLOAD_ACTION_PREFIX));
238 if !known {
239 return Err(format!(
240 "permit rule '{}': action selector '{a}' is not a governed workload action",
241 self.id
242 ));
243 }
244 }
245 if self.kinds.iter().any(|k| k.is_empty()) || self.accelerators.iter().any(|a| a.is_empty()) {
246 return Err(format!("permit rule '{}': empty kind or accelerator selector", self.id));
247 }
248 if self.min_package_trust == PackageTrust::Unsigned && self.principals.is_empty() {
249 return Err(format!(
250 "permit rule '{}' accepts unsigned packages, so it must name the principals it \
251 applies to (e.g. [\"{CATALOG_PRINCIPAL}\"] for the catalog verbs)",
252 self.id
253 ));
254 }
255 if self.principals.iter().any(|p| p.is_empty()) {
256 return Err(format!("permit rule '{}': empty principal selector", self.id));
257 }
258 if self.projects.iter().any(|p| p.is_empty()) {
259 return Err(format!("permit rule '{}': empty project selector", self.id));
260 }
261 if !self.max_resource_cost.is_finite() || !(0.0..=1.0).contains(&self.max_resource_cost) {
262 return Err(format!("permit rule '{}': max_resource_cost must be in [0, 1]", self.id));
263 }
264 Ok(())
265 }
266
267 pub fn matches(&self, action: &str, effect: &WorkloadEffect) -> bool {
269 self.matches_in(action, effect, None)
270 }
271
272 pub fn matches_in(&self, action: &str, effect: &WorkloadEffect, project: Option<&str>) -> bool {
275 self.matches_for(action, effect, project, None)
276 }
277
278 pub fn matches_for(
282 &self,
283 action: &str,
284 effect: &WorkloadEffect,
285 project: Option<&str>,
286 principal: Option<&str>,
287 ) -> bool {
288 let principal_ok =
289 self.principals.is_empty() || principal.is_some_and(|p| self.principals.iter().any(|x| x == p));
290 let project_ok = self.projects.is_empty() || project.is_some_and(|p| self.projects.iter().any(|x| x == p));
291 let action_ok = self.actions.iter().any(|s| selector_matches(s, action));
292 let kind_ok = self.kinds.iter().any(|k| k == "*" || k == &effect.kind);
293 let accel_ok = match &effect.accelerator {
294 None => true,
295 Some(acc) => self.accelerators.iter().any(|s| selector_matches(s, acc)),
296 };
297 principal_ok
298 && project_ok
299 && action_ok
300 && kind_ok
301 && accel_ok
302 && effect.package_trust >= self.min_package_trust
303 && effect.node_tier >= self.min_node_tier
304 && effect.network <= self.max_network
305 && (self.allow_secrets || !effect.secrets)
306 && (self.allow_emulated || !effect.emulated)
307 && effect.resource_cost <= self.max_resource_cost
308 }
309}