Skip to main content

soma_domain/
execution.rs

1use serde::{Deserialize, Serialize};
2
3const MAX_REQUEST_ID_BYTES: usize = 128;
4
5/// The entry point through which a request reached the service.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "kebab-case")]
8pub enum Surface {
9    /// The MCP protocol surface.
10    Mcp,
11    /// The REST HTTP surface.
12    Rest,
13    /// The command-line interface surface.
14    Cli,
15    /// The command palette surface.
16    Palette,
17}
18
19/// How the request's authorization was established.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "kebab-case")]
22pub enum AuthorizationMode {
23    /// Loopback development mode with auth bypassed.
24    LoopbackDev,
25    /// Behind a trusted gateway that enforces authorization upstream.
26    TrustedGateway,
27    /// Full auth middleware mounted (bearer token or OAuth).
28    Mounted,
29}
30
31/// Whether a destructive operation has been explicitly confirmed by the caller.
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "kebab-case")]
34pub enum Confirmation {
35    /// No confirmation was provided (the default).
36    #[default]
37    Missing,
38    /// The caller explicitly confirmed the operation.
39    Confirmed,
40}
41
42impl Confirmation {
43    /// Returns `true` when the operation has been confirmed.
44    pub fn is_confirmed(self) -> bool {
45        matches!(self, Self::Confirmed)
46    }
47}
48
49/// A validated request identifier: non-empty, bounded in length, and free of
50/// control characters.
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(transparent)]
53pub struct RequestId(String);
54
55impl RequestId {
56    /// Validates and constructs a `RequestId`, rejecting empty, over-long, or
57    /// control-character-containing values.
58    pub fn new(value: impl Into<String>) -> Result<Self, RequestIdError> {
59        let value = value.into();
60        if value.trim().is_empty() {
61            return Err(RequestIdError::Empty);
62        }
63        if value.len() > MAX_REQUEST_ID_BYTES {
64            return Err(RequestIdError::TooLong {
65                actual: value.len(),
66                maximum: MAX_REQUEST_ID_BYTES,
67            });
68        }
69        if value.chars().any(char::is_control) {
70            return Err(RequestIdError::ControlCharacter);
71        }
72        Ok(Self(value))
73    }
74
75    /// Returns the request id as a string slice.
76    pub fn as_str(&self) -> &str {
77        &self.0
78    }
79}
80
81/// Reasons a [`RequestId`] can fail validation.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum RequestIdError {
84    /// The value was empty or whitespace-only.
85    Empty,
86    /// The value exceeded the maximum allowed byte length.
87    TooLong {
88        /// Actual byte length of the supplied value.
89        actual: usize,
90        /// Maximum permitted byte length.
91        maximum: usize,
92    },
93    /// The value contained a control character.
94    ControlCharacter,
95}
96
97impl std::fmt::Display for RequestIdError {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        match self {
100            Self::Empty => f.write_str("request id must not be empty"),
101            Self::TooLong { actual, maximum } => {
102                write!(f, "request id is {actual} bytes; maximum is {maximum}")
103            }
104            Self::ControlCharacter => f.write_str("request id must not contain control characters"),
105        }
106    }
107}
108
109impl std::error::Error for RequestIdError {}
110
111/// W3C Trace Context propagation fields carried with a request.
112#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
113pub struct TraceContext {
114    /// The `traceparent` header value, if present.
115    pub traceparent: Option<String>,
116    /// The `tracestate` header value, if present.
117    pub tracestate: Option<String>,
118}
119
120#[cfg(test)]
121mod tests {
122    use super::{Confirmation, RequestId};
123
124    #[test]
125    fn request_ids_reject_empty_and_control_characters() {
126        assert!(RequestId::new("  ").is_err());
127        assert!(RequestId::new("request\n1").is_err());
128        assert_eq!(RequestId::new("request-1").unwrap().as_str(), "request-1");
129    }
130
131    #[test]
132    fn confirmation_defaults_to_missing() {
133        assert!(!Confirmation::default().is_confirmed());
134        assert!(Confirmation::Confirmed.is_confirmed());
135    }
136}