Skip to main content

deepstrike_core/governance/
quota.rs

1//! Declarative resource quotas evaluated at the single syscall trap (M2 资源配额).
2//!
3//! The syscall gate ([`crate::scheduler::state_machine::LoopStateMachine::gate_syscall`]) is the
4//! one chokepoint where effectful requests (`Invoke`/`Spawn`/`WriteMemory`/…) are adjudicated.
5//! Governance rules already gate tool *invocation*; this adds the OS notion of **resource
6//! quotas** to the *same* gate — without a new ABI shape — so spawning and memory writes become
7//! bounded resources rather than unconditional `Allow`s.
8//!
9//! The kernel stays pure: a quota is declarative config + the facts the kernel already tracks
10//! (running child tasks in the `TaskTable`, write timestamps from the observed clock). No I/O.
11
12use serde::{Deserialize, Serialize};
13
14/// Opt-in resource limits. An unset field imposes no limit; an unset `ResourceQuota` (the default,
15/// when [`crate::scheduler::state_machine::LoopStateMachine::set_resource_quota`] is never called)
16/// preserves the pre-M2 behavior of unconditional `Allow` for spawn / memory syscalls.
17#[derive(Debug, Clone, Default, Serialize, Deserialize)]
18pub struct ResourceQuota {
19    /// Max sub-agents in the `Running` state at once. Further `Spawn`s are denied while at cap.
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub max_concurrent_subagents: Option<u32>,
22    /// Max sub-agent nesting depth (direct children of the root loop are depth 1).
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub max_spawn_depth: Option<u32>,
25    /// Rolling-window memory-write rate limit as `(max_writes, window_ms)`: at most `max_writes`
26    /// successful `WriteMemory` syscalls may occur within any `window_ms` span.
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub memory_writes_per_window: Option<(u32, u64)>,
29    /// R3-1: max total nodes a single workflow DAG may grow to via runtime `SubmitNodes`. Once the
30    /// DAG (existing + submitted) would exceed this, the submission is denied — a backstop against an
31    /// unbounded loop-until-done. `None` = no cap.
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub max_workflow_nodes: Option<usize>,
34}
35
36impl ResourceQuota {
37    /// Whether any limit is actually set (used to short-circuit the gate when fully open).
38    pub fn is_open(&self) -> bool {
39        self.max_concurrent_subagents.is_none()
40            && self.max_spawn_depth.is_none()
41            && self.memory_writes_per_window.is_none()
42            && self.max_workflow_nodes.is_none()
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    #[test]
51    fn default_quota_is_open() {
52        assert!(ResourceQuota::default().is_open());
53    }
54
55    #[test]
56    fn any_set_limit_closes_the_quota() {
57        let q = ResourceQuota { max_concurrent_subagents: Some(2), ..Default::default() };
58        assert!(!q.is_open());
59    }
60}