Skip to main content

deepstrike_core/syscall/
mod.rs

1//! Primitive P1: the single syscall trap boundary.
2//!
3//! M0 scaffold (see `.local-docs/specs/agent-os-three-primitives.md`): types + conversions
4//! only — **no wiring, no behavior change**. A later milestone (M2) generalizes
5//! [`crate::governance::pipeline`] so its request becomes [`Syscall`] and its result becomes
6//! [`Disposition`], and routes spawn / page-in / write-memory through the same gate (today they
7//! bypass governance entirely).
8//!
9//! Concept overlap this primitive collapses: the two parallel decision vocabularies
10//! ([`crate::types::policy::GovernanceVerdict`] and `SignalDisposition`). Tool/spawn/memory
11//! decisions converge on [`Disposition`]; signals feed the P2 scheduler instead.
12
13use crate::mm::memory::MemoryWriteRequest;
14use crate::scheduler::tcb::WaitReason;
15use crate::types::agent::IsolationManifest;
16use crate::types::message::ToolCall;
17use crate::types::policy::GovernanceVerdict;
18
19/// An effectful request from the SDK that the kernel must adjudicate.
20///
21/// Every side-effecting service request becomes a `Syscall` variant; the opcode is **data**, so
22/// adding a service does not add a new ABI shape (unlike the per-feature `Load*Policy` events today).
23#[derive(Debug, Clone)]
24pub enum Syscall {
25    /// Model-proposed tool call (today: the only thing through the governance gate).
26    Invoke(ToolCall),
27    /// Spawn a sub-agent (today: bypasses the gate).
28    Spawn(IsolationManifest),
29    /// Persist a long-term memory entry.
30    WriteMemory(MemoryWriteRequest),
31    /// R3-1: append `count` nodes to the in-flight workflow DAG at runtime. Gating DAG growth through
32    /// the trap lets a `ResourceQuota` backstop a runaway loop-until-done (denied past
33    /// `max_workflow_nodes`); per-node spawns are still gated separately by `Spawn`.
34    SubmitNodes { count: usize },
35    /// M5/G1: an agent authors a whole workflow `spec` (`node_count` nodes). Bootstraps the DAG when
36    /// none is active, else flattens onto it — either way it is gated by the same `max_workflow_nodes`
37    /// quota as `SubmitNodes` (a spec is just a node batch with a bootstrap fast-path), so an
38    /// agent-authored harness cannot overgrow the DAG past the run's budget.
39    LoadWorkflow { node_count: usize },
40}
41
42/// The kernel's adjudication of a [`Syscall`]. Generalizes [`GovernanceVerdict`]:
43/// `AskUser` becomes [`Disposition::Gate`] (suspend the calling task via the P2 TCB),
44/// which is where this primitive meets P2.
45#[derive(Debug, Clone)]
46pub enum Disposition {
47    /// Proceed as requested.
48    Allow,
49    /// Reject. `stage` names the gate stage that vetoed.
50    Deny { stage: &'static str, reason: String },
51    /// Suspend the calling task until an external party resolves it (e.g. human approval).
52    /// `reason` carries the human-readable justification (e.g. the governance `AskUser` reason).
53    Gate { wait: WaitReason, reason: String },
54    /// Accept but queue for later scheduling (backpressure).
55    Defer { slot: u32 },
56    /// Rejected by a rate limiter; retry permitted after the delay.
57    RateLimited { retry_after_ms: u64 },
58}
59
60impl Disposition {
61    /// Whether the syscall may proceed to execution now.
62    pub fn is_allowed(&self) -> bool {
63        matches!(self, Self::Allow)
64    }
65}
66
67/// Bridge from the existing tool-decision vocabulary. `AskUser` → `Gate(Approval)`: a tool
68/// awaiting human approval suspends the task, which M2+M1 realize via the TCB.
69impl From<GovernanceVerdict> for Disposition {
70    fn from(verdict: GovernanceVerdict) -> Self {
71        match verdict {
72            GovernanceVerdict::Allow => Disposition::Allow,
73            GovernanceVerdict::Deny { stage, reason } => Disposition::Deny { stage, reason },
74            GovernanceVerdict::RateLimited { retry_after_ms } => {
75                Disposition::RateLimited { retry_after_ms }
76            }
77            GovernanceVerdict::AskUser { reason } => Disposition::Gate {
78                wait: WaitReason::Approval,
79                reason,
80            },
81        }
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn verdict_allow_maps_to_allow() {
91        let d: Disposition = GovernanceVerdict::Allow.into();
92        assert!(d.is_allowed());
93    }
94
95    #[test]
96    fn verdict_deny_preserves_stage_and_reason() {
97        let d: Disposition = GovernanceVerdict::Deny {
98            stage: "veto",
99            reason: "blocked".into(),
100        }
101        .into();
102        match d {
103            Disposition::Deny { stage, reason } => {
104                assert_eq!(stage, "veto");
105                assert_eq!(reason, "blocked");
106            }
107            other => panic!("expected Deny, got {other:?}"),
108        }
109        assert!(!Disposition::Deny { stage: "veto", reason: String::new() }.is_allowed());
110    }
111
112    #[test]
113    fn verdict_ask_user_maps_to_gate_approval() {
114        let d: Disposition = GovernanceVerdict::AskUser {
115            reason: "confirm".into(),
116        }
117        .into();
118        assert!(matches!(
119            &d,
120            Disposition::Gate { wait: WaitReason::Approval, reason } if reason == "confirm"
121        ));
122        assert!(!d.is_allowed());
123    }
124
125    #[test]
126    fn verdict_rate_limited_preserves_delay() {
127        let d: Disposition = GovernanceVerdict::RateLimited { retry_after_ms: 500 }.into();
128        assert!(matches!(d, Disposition::RateLimited { retry_after_ms: 500 }));
129    }
130
131}