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/// F5 projection pair (registered in `crate::projection_pairs`, 0.2.66): the wire
15/// version is the ABI authority; this is the richer internal semantic vocabulary. The
16/// only legal crossing is the driver's exhaustive conversion.
17/// Opt-in resource limits. An unset field imposes no limit; an unset `ResourceQuota` (the default,
18/// when [`crate::scheduler::state_machine::LoopStateMachine::set_resource_quota`] is never called)
19/// preserves the pre-M2 behavior of unconditional `Allow` for spawn / memory syscalls.
20#[derive(Debug, Clone, Default, Serialize, Deserialize)]
21pub struct ResourceQuota {
22 /// Max sub-agents in the `Running` state at once. Further `Spawn`s are denied while at cap.
23 /// *Instantaneous* — vehicle-scoped (cannot span stateless replicas; spec §2.5).
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub max_concurrent_subagents: Option<u32>,
26 /// L1 (RunGroup): max sub-agents spawned *cumulatively* over the whole governance domain. Unlike
27 /// `max_concurrent_subagents` this counts every spawn ever (running + completed), seeded across
28 /// members via reservation-backed budget grants, so it spans N stateless top-level runs. A hard `Deny` at cap
29 /// (a completed sibling never frees a cumulative slot).
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub max_total_subagents: Option<u32>,
32 /// Max sub-agent nesting depth (direct children of the root loop are depth 1).
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub max_spawn_depth: Option<u32>,
35 /// Rolling-window memory-write rate limit as `(max_writes, window_ms)`: at most `max_writes`
36 /// successful `WriteMemory` syscalls may occur within any `window_ms` span.
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub memory_writes_per_window: Option<(u32, u64)>,
39 /// R3-1: max total nodes a single workflow DAG may grow to via runtime `SubmitNodes`. Once the
40 /// DAG (existing + submitted) would exceed this, the submission is denied — a backstop against an
41 /// unbounded loop-until-done. `None` = no cap.
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub max_workflow_nodes: Option<usize>,
44}