Skip to main content

serve/
config.rs

1//! `serve.toml`: the few facts about an app that are not code.
2//!
3//! ```toml
4//! name = "revenue-analyst"          # defaults to the Cargo package name
5//!
6//! [sandbox]
7//! kind = "bashkit"                  # none | local | bashkit | microvm
8//!
9//! [secrets]
10//! required = ["WAREHOUSE_URL"]      # beyond those connections/channels declare
11//!
12//! [deploy]
13//! target = "everruns-cloud"
14//! ```
15
16use serde::{Deserialize, Serialize};
17
18/// Parsed `serve.toml`.
19#[derive(Clone, Debug, Default, Deserialize, Serialize)]
20#[serde(deny_unknown_fields)]
21pub struct AppConfig {
22    /// App name; defaults to the binary's Cargo package name.
23    #[serde(default)]
24    pub name: Option<String>,
25    #[serde(default)]
26    pub sandbox: SandboxConfig,
27    #[serde(default)]
28    pub secrets: SecretsConfig,
29    #[serde(default)]
30    pub deploy: DeployConfig,
31}
32
33/// `[sandbox]`.
34#[derive(Clone, Debug, Default, Deserialize, Serialize)]
35#[serde(deny_unknown_fields)]
36pub struct SandboxConfig {
37    #[serde(default)]
38    pub kind: SandboxKind,
39}
40
41/// Where the agent's shell and files run. The code never changes between
42/// kinds; the host (or `dev`) supplies the adapter.
43#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
44#[serde(rename_all = "lowercase")]
45pub enum SandboxKind {
46    /// No shell or filesystem tools.
47    #[default]
48    None,
49    /// Files in a real directory under the app's data dir.
50    Local,
51    /// A virtual bash over the session filesystem (bashkit).
52    Bashkit,
53    /// An isolated microVM supplied by the host. `dev` substitutes bashkit.
54    Microvm,
55}
56
57impl SandboxKind {
58    pub fn as_str(&self) -> &'static str {
59        match self {
60            SandboxKind::None => "none",
61            SandboxKind::Local => "local",
62            SandboxKind::Bashkit => "bashkit",
63            SandboxKind::Microvm => "microvm",
64        }
65    }
66}
67
68/// `[secrets]`.
69#[derive(Clone, Debug, Default, Deserialize, Serialize)]
70#[serde(deny_unknown_fields)]
71pub struct SecretsConfig {
72    #[serde(default)]
73    pub required: Vec<String>,
74}
75
76/// `[deploy]`.
77#[derive(Clone, Debug, Default, Deserialize, Serialize)]
78#[serde(deny_unknown_fields)]
79pub struct DeployConfig {
80    #[serde(default)]
81    pub target: Option<String>,
82}
83
84impl AppConfig {
85    pub fn parse(text: &str) -> crate::Result<Self> {
86        Ok(toml::from_str(text)?)
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn parses_full_config() {
96        let config = AppConfig::parse(
97            r#"
98            name = "revenue-analyst"
99            [sandbox]
100            kind = "bashkit"
101            [secrets]
102            required = ["WAREHOUSE_URL"]
103            [deploy]
104            target = "everruns-cloud"
105            "#,
106        )
107        .unwrap();
108        assert_eq!(config.name.as_deref(), Some("revenue-analyst"));
109        assert_eq!(config.sandbox.kind, SandboxKind::Bashkit);
110        assert_eq!(config.secrets.required, vec!["WAREHOUSE_URL"]);
111    }
112
113    #[test]
114    fn empty_config_defaults_to_no_sandbox() {
115        let config = AppConfig::parse("").unwrap();
116        assert_eq!(config.sandbox.kind, SandboxKind::None);
117    }
118
119    #[test]
120    fn unknown_keys_are_rejected() {
121        assert!(AppConfig::parse("sandbx = 1").is_err());
122    }
123}