Skip to main content

deepstrike_sdk/runtime/
os_profile.rs

1use deepstrike_core::runtime::kernel::{
2    ConstraintSpec, KernelInputEvent, PolicyAction, PolicyRule, RateLimitSpec, SignalPolicyConfig,
3    SIGNAL_POLICY_VERSION,
4};
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) -> SignalPolicyConfig {
18        SignalPolicyConfig {
19            version: SIGNAL_POLICY_VERSION,
20            queue_max: self.queue_max,
21            ttl_ms: self.ttl_ms,
22            deadline_escalation: self.deadline_escalation,
23        }
24    }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct MemoryWriteRateLimit {
29    pub max_writes: u32,
30    pub window_ms: u64,
31}
32
33impl From<MemoryWriteRateLimit> for (u32, u64) {
34    fn from(limit: MemoryWriteRateLimit) -> Self {
35        (limit.max_writes, limit.window_ms)
36    }
37}
38
39#[derive(Debug, Clone)]
40pub struct GovernancePolicy {
41    pub default_action: Option<PolicyAction>,
42    pub rules: Vec<PolicyRule>,
43    pub vetoed_tools: Vec<String>,
44    pub rate_limits: Vec<RateLimitSpec>,
45    pub constraints: Vec<ConstraintSpec>,
46    /// I5: when true (default), the runner pre-filters denied tools from the schema. Mirrors Node.
47    pub surface_denied_in_system: bool,
48}
49
50impl GovernancePolicy {
51    pub fn allow_all() -> Self {
52        Self {
53            default_action: None,
54            rules: vec![PolicyRule {
55                tool_pattern: "*".to_string(),
56                action: PolicyAction::Allow,
57            }],
58            vetoed_tools: vec![],
59            rate_limits: vec![],
60            constraints: vec![],
61            surface_denied_in_system: true,
62        }
63    }
64
65    pub fn into_kernel_event(self) -> KernelInputEvent {
66        KernelInputEvent::LoadGovernancePolicy {
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}