Skip to main content

deepstrike_sdk/runtime/
os_profile.rs

1pub use deepstrike_core::runtime::kernel::wire::{
2    ParamConstraint as ConstraintSpec, PolicyAction, PolicyRule, RateLimitSpec,
3};
4use deepstrike_core::runtime::kernel::wire::{SignalPolicy as WireSignalPolicy, WireU64};
5pub use deepstrike_core::scheduler::policy::SchedulerPolicyConfig;
6
7use crate::{Error, Result};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct SignalPolicy {
11    pub queue_max: u32,
12    pub ttl_ms: Option<u64>,
13    pub deadline_escalation: Option<bool>,
14}
15
16impl SignalPolicy {
17    pub(crate) fn into_kernel(self) -> WireSignalPolicy {
18        WireSignalPolicy {
19            queue_max: self.queue_max,
20            ttl_ms: self.ttl_ms.map(WireU64::new),
21            deadline_escalation: self.deadline_escalation,
22        }
23    }
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct MemoryWriteRateLimit {
28    pub max_writes: u32,
29    pub window_ms: u64,
30}
31
32impl From<MemoryWriteRateLimit> for (u32, u64) {
33    fn from(limit: MemoryWriteRateLimit) -> Self {
34        (limit.max_writes, limit.window_ms)
35    }
36}
37
38#[derive(Debug, Clone)]
39pub struct GovernancePolicy {
40    pub default_action: Option<PolicyAction>,
41    pub rules: Vec<PolicyRule>,
42    pub vetoed_tools: Vec<String>,
43    pub rate_limits: Vec<RateLimitSpec>,
44    pub constraints: Vec<ConstraintSpec>,
45    /// I5: when true (default), the runner pre-filters denied tools from the schema. Mirrors Node.
46    pub surface_denied_in_system: bool,
47}
48
49impl GovernancePolicy {
50    pub fn allow_all() -> Self {
51        Self {
52            default_action: None,
53            rules: vec![PolicyRule {
54                tool_pattern: "*".to_string(),
55                action: PolicyAction::Allow,
56            }],
57            vetoed_tools: vec![],
58            rate_limits: vec![],
59            constraints: vec![],
60            surface_denied_in_system: true,
61        }
62    }
63
64    pub(crate) fn into_host_fact(self) -> serde_json::Value {
65        serde_json::json!({
66            "kind": "load_governance_policy",
67            "default_action": self.default_action,
68            "rules": self.rules,
69            "vetoed_tools": self.vetoed_tools,
70            "rate_limits": self.rate_limits,
71            "constraints": self.constraints,
72        })
73    }
74}
75
76#[derive(Debug, Clone)]
77pub struct NativeOsProfile {
78    pub id: &'static str,
79    pub signal_policy: SignalPolicy,
80    pub governance_policy: GovernancePolicy,
81}
82
83#[derive(Debug, Clone)]
84pub enum OsProfile {
85    Native,
86    Concrete(NativeOsProfile),
87}
88
89pub const DEFAULT_NATIVE_SIGNAL_POLICY: SignalPolicy = SignalPolicy {
90    queue_max: 64,
91    ttl_ms: None,
92    deadline_escalation: None,
93};
94
95pub fn default_native_governance_policy() -> GovernancePolicy {
96    GovernancePolicy::allow_all()
97}
98
99/// I5: bucket tool schemas into allowed/denied per policy. Pure. Mirrors Node `governanceFilterSchema`.
100pub fn governance_filter_schema(
101    tools: &[deepstrike_core::types::message::ToolSchema],
102    policy: &GovernancePolicy,
103) -> (
104    Vec<deepstrike_core::types::message::ToolSchema>,
105    Vec<String>,
106) {
107    let mut allowed = Vec::with_capacity(tools.len());
108    let mut denied = Vec::new();
109    let matches = |pat: &str, name: &str| -> bool {
110        if pat == name {
111            return true;
112        }
113        if let Some(prefix) = pat.strip_suffix('*') {
114            return name.starts_with(prefix);
115        }
116        false
117    };
118    for tool in tools {
119        let name = tool.name.as_str();
120        if policy.vetoed_tools.iter().any(|v| v == name) {
121            denied.push(name.to_string());
122            continue;
123        }
124        let mut action = policy.default_action.clone().unwrap_or(PolicyAction::Allow);
125        for r in &policy.rules {
126            if matches(&r.tool_pattern, name) {
127                action = r.action.clone();
128            }
129        }
130        if matches!(action, PolicyAction::Deny) {
131            denied.push(name.to_string());
132        } else {
133            allowed.push(tool.clone());
134        }
135    }
136    (allowed, denied)
137}
138
139pub fn os_profile(profile: Option<OsProfile>) -> NativeOsProfile {
140    match profile.unwrap_or(OsProfile::Native) {
141        OsProfile::Native => NativeOsProfile {
142            id: "native",
143            signal_policy: DEFAULT_NATIVE_SIGNAL_POLICY,
144            governance_policy: default_native_governance_policy(),
145        },
146        OsProfile::Concrete(profile) => profile,
147    }
148}
149
150pub fn assert_native_profile(profile: Option<OsProfile>) -> Result<NativeOsProfile> {
151    let resolved = os_profile(profile);
152    if resolved.id != "native" {
153        return Err(Error::Other(format!(
154            "Unsupported OS profile: {}",
155            resolved.id
156        )));
157    }
158    if resolved.signal_policy.queue_max == 0 {
159        return Err(Error::Other(
160            "Invalid native OS profile: SignalPolicy queue_max must be positive".to_string(),
161        ));
162    }
163    if matches!(resolved.signal_policy.ttl_ms, Some(0)) {
164        return Err(Error::Other(
165            "Invalid native OS profile: SignalPolicy ttl_ms must be positive when present"
166                .to_string(),
167        ));
168    }
169    Ok(resolved)
170}