use serde::{Deserialize, Serialize};
use crate::governance::EffectVector;
pub const MAX_KIND_LEN: usize = 64;
pub const MAX_ACCEL_LEN: usize = 128;
pub const MAX_REFS: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PackageTrust {
Unsigned,
AdoptedUnverified,
ProjectCert,
SignedUnpinned,
OperatorAttested,
PinnedSigner,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NodeTrustTier {
Discovered,
Paired,
Pinned,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NetworkPolicy {
None,
Lan,
Egress,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WorkloadEffect {
pub kind: String,
pub package_trust: PackageTrust,
pub node_tier: NodeTrustTier,
pub network: NetworkPolicy,
#[serde(default)]
pub secrets: bool,
#[serde(default)]
pub emulated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub accelerator: Option<String>,
#[serde(default)]
pub resource_cost: f64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkloadRefs {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub package_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub signer_keys: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub artifact_hashes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct WorkloadRequest {
pub effect: WorkloadEffect,
pub refs: WorkloadRefs,
}
fn is_kind_char(c: char) -> bool {
c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_'
}
fn is_capability_id(s: &str) -> bool {
!s.is_empty()
&& s.len() <= MAX_ACCEL_LEN
&& !s.starts_with('.')
&& !s.ends_with('.')
&& !s.contains("..")
&& s
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '.' | '-' | '_'))
}
impl WorkloadEffect {
pub fn validate(&self) -> Result<(), String> {
if self.kind.is_empty() || self.kind.len() > MAX_KIND_LEN {
return Err(format!("workload kind must be 1..={MAX_KIND_LEN} chars"));
}
if !self.kind.chars().all(is_kind_char) {
return Err(format!(
"workload kind '{}' must be lowercase [a-z0-9_-]",
self.kind
));
}
if let Some(acc) = &self.accelerator
&& !is_capability_id(acc)
{
return Err(format!("accelerator '{acc}' is not a dotted lowercase capability id"));
}
if !self.resource_cost.is_finite() || !(0.0..=1.0).contains(&self.resource_cost) {
return Err(format!(
"resource_cost {} must be a finite value in [0.0, 1.0]",
self.resource_cost
));
}
Ok(())
}
pub fn to_effect_vector(&self) -> EffectVector {
let clamp = |v: f64| v.clamp(0.0, 1.0);
let trust_risk = match self.package_trust {
PackageTrust::Unsigned => 0.9,
PackageTrust::AdoptedUnverified => 0.5,
PackageTrust::SignedUnpinned => 0.6,
PackageTrust::OperatorAttested => 0.3,
PackageTrust::PinnedSigner => 0.1,
PackageTrust::ProjectCert => 0.2,
};
let tier_sec = match self.node_tier {
NodeTrustTier::Discovered => 0.8,
NodeTrustTier::Paired => 0.3,
NodeTrustTier::Pinned => 0.1,
};
let net_sec = match self.network {
NetworkPolicy::None => 0.0,
NetworkPolicy::Lan => 0.1,
NetworkPolicy::Egress => 0.3,
};
let egress_priv = if self.network == NetworkPolicy::Egress { 0.2 } else { 0.0 };
EffectVector {
risk: clamp(trust_risk + if self.emulated { 0.1 } else { 0.0 }),
fairness: clamp(self.resource_cost),
privacy: clamp(if self.secrets { 0.5 } else { 0.0 } + egress_priv),
novelty: clamp(
if self.emulated { 0.4 } else { 0.0 }
+ if self.accelerator.is_some() { 0.2 } else { 0.0 },
),
security: clamp(tier_sec + net_sec),
}
}
pub fn context_map(&self) -> std::collections::HashMap<String, String> {
let to_s = |v: serde_json::Value| v.as_str().map(str::to_owned).unwrap_or_default();
let mut m = std::collections::HashMap::new();
m.insert("kind".into(), self.kind.clone());
m.insert("package_trust".into(), to_s(serde_json::json!(self.package_trust)));
m.insert("node_tier".into(), to_s(serde_json::json!(self.node_tier)));
m.insert("network".into(), to_s(serde_json::json!(self.network)));
m.insert("secrets".into(), self.secrets.to_string());
m.insert("emulated".into(), self.emulated.to_string());
m.insert(
"accelerator".into(),
self.accelerator.clone().unwrap_or_default(),
);
m.insert("resource_cost".into(), self.resource_cost.to_string());
m
}
}
impl WorkloadRequest {
pub fn from_context(context: &serde_json::Value) -> Result<Self, String> {
let obj = context
.get("workload")
.ok_or_else(|| "missing 'workload' object in gate context".to_owned())?;
if !obj.is_object() {
return Err("'workload' must be a JSON object".into());
}
let effect: WorkloadEffect =
serde_json::from_value(obj.clone()).map_err(|e| format!("invalid workload effect: {e}"))?;
let refs: WorkloadRefs =
serde_json::from_value(obj.clone()).map_err(|e| format!("invalid workload refs: {e}"))?;
effect.validate()?;
if refs.signer_keys.len() > MAX_REFS || refs.artifact_hashes.len() > MAX_REFS {
return Err(format!("at most {MAX_REFS} signer keys and artifact hashes"));
}
use crate::revocation::RevocationKind;
if let Some(p) = &refs.package_id {
RevocationKind::Package.normalize(p).map_err(|e| e.to_string())?;
}
for k in &refs.signer_keys {
RevocationKind::SignerKey.normalize(k).map_err(|e| e.to_string())?;
}
for h in &refs.artifact_hashes {
RevocationKind::ArtifactHash.normalize(h).map_err(|e| e.to_string())?;
}
Ok(Self { effect, refs })
}
}