use crate::backend::MobBackendKind;
use crate::runtime_mode::MobRuntimeMode;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolConfig {
#[serde(default)]
pub builtins: bool,
#[serde(default)]
pub shell: bool,
#[serde(default)]
pub comms: bool,
#[serde(default)]
pub memory: bool,
#[serde(default)]
pub mob: bool,
#[serde(default)]
pub mob_tasks: bool,
#[serde(default)]
pub schedule: bool,
#[serde(default)]
pub mcp: Vec<String>,
#[serde(default)]
pub rust_bundles: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ProfileBinding {
RealmRef {
realm_profile: String,
},
Inline(Profile),
}
impl ProfileBinding {
pub fn as_inline(&self) -> Option<&Profile> {
match self {
Self::Inline(p) => Some(p),
Self::RealmRef { .. } => None,
}
}
pub fn as_inline_mut(&mut self) -> Option<&mut Profile> {
match self {
Self::Inline(p) => Some(p),
Self::RealmRef { .. } => None,
}
}
pub fn realm_ref_name(&self) -> Option<&str> {
match self {
Self::RealmRef { realm_profile } => Some(realm_profile),
Self::Inline(_) => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "mode", rename_all = "snake_case")]
pub enum SpawnTooling {
InheritParent {
#[serde(default, skip_serializing_if = "Option::is_none")]
allow_overlay: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
deny_overlay: Option<Vec<String>>,
},
Minimal,
Profile {
source: Box<ProfileSource>,
#[serde(default, skip_serializing_if = "Option::is_none")]
allow_overlay: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
deny_overlay: Option<Vec<String>>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ProfileSource {
RealmProfile {
name: String,
},
Inline(Profile),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Profile {
pub model: String,
#[serde(default)]
pub skills: Vec<String>,
#[serde(default)]
pub tools: ToolConfig,
#[serde(default)]
pub peer_description: String,
#[serde(default)]
pub external_addressable: bool,
#[serde(default)]
pub backend: Option<MobBackendKind>,
#[serde(default)]
pub runtime_mode: MobRuntimeMode,
#[serde(default)]
pub max_inline_peer_notifications: Option<i32>,
#[serde(default)]
pub output_schema: Option<serde_json::Value>,
#[serde(default)]
pub provider_params: Option<serde_json::Value>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_config_serde_roundtrip() {
let config = ToolConfig {
builtins: true,
shell: false,
comms: true,
memory: false,
mob: true,
mob_tasks: true,
schedule: true,
mcp: vec!["server-a".to_string(), "server-b".to_string()],
rust_bundles: vec!["custom-tools".to_string()],
};
let json = serde_json::to_string(&config).unwrap();
let parsed: ToolConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, config);
}
#[test]
fn test_tool_config_toml_roundtrip() {
let config = ToolConfig {
builtins: true,
shell: true,
comms: false,
memory: false,
mob: false,
mob_tasks: false,
schedule: false,
mcp: vec!["mcp-server".to_string()],
rust_bundles: Vec::new(),
};
let toml_str = toml::to_string(&config).unwrap();
let parsed: ToolConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(parsed, config);
}
#[test]
fn test_profile_serde_roundtrip() {
let profile = Profile {
model: "claude-opus-4-6".to_string(),
skills: vec!["orchestrator-skill".to_string()],
tools: ToolConfig {
builtins: true,
shell: false,
comms: true,
memory: false,
mob: true,
mob_tasks: true,
schedule: false,
mcp: vec![],
rust_bundles: vec![],
},
peer_description: "Orchestrates worker agents".to_string(),
external_addressable: true,
backend: None,
runtime_mode: MobRuntimeMode::AutonomousHost,
max_inline_peer_notifications: None,
output_schema: None,
provider_params: None,
};
let json = serde_json::to_string(&profile).unwrap();
let parsed: Profile = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, profile);
}
#[test]
fn test_profile_toml_roundtrip() {
let profile = Profile {
model: "gpt-5.2".to_string(),
skills: vec!["worker-skill".to_string()],
tools: ToolConfig {
builtins: false,
shell: true,
comms: true,
memory: false,
mob: false,
mob_tasks: true,
schedule: false,
mcp: vec!["code-server".to_string()],
rust_bundles: vec!["custom".to_string()],
},
peer_description: "Writes code".to_string(),
external_addressable: false,
backend: Some(MobBackendKind::External),
runtime_mode: MobRuntimeMode::TurnDriven,
max_inline_peer_notifications: Some(20),
output_schema: None,
provider_params: None,
};
let toml_str = toml::to_string(&profile).unwrap();
let parsed: Profile = toml::from_str(&toml_str).unwrap();
assert_eq!(parsed, profile);
}
#[test]
fn test_tool_config_defaults() {
let config = ToolConfig::default();
assert!(!config.builtins);
assert!(!config.shell);
assert!(!config.comms);
assert!(!config.memory);
assert!(!config.mob);
assert!(!config.mob_tasks);
assert!(!config.schedule);
assert!(config.mcp.is_empty());
assert!(config.rust_bundles.is_empty());
}
#[test]
fn test_profile_default_fields_from_toml() {
let toml_str = r#"
model = "claude-sonnet-4-5"
"#;
let profile: Profile = toml::from_str(toml_str).unwrap();
assert_eq!(profile.model, "claude-sonnet-4-5");
assert!(profile.skills.is_empty());
assert_eq!(profile.tools, ToolConfig::default());
assert_eq!(profile.peer_description, "");
assert!(!profile.external_addressable);
assert_eq!(profile.backend, None);
assert_eq!(profile.runtime_mode, MobRuntimeMode::AutonomousHost);
assert_eq!(profile.max_inline_peer_notifications, None);
assert_eq!(profile.provider_params, None);
}
#[test]
fn test_profile_toml_parses_zero_inline_threshold() {
let toml_str = r#"
model = "claude-sonnet-4-5"
max_inline_peer_notifications = 0
"#;
let profile: Profile = toml::from_str(toml_str).unwrap();
assert_eq!(profile.max_inline_peer_notifications, Some(0));
}
#[test]
fn test_profile_toml_parses_always_inline_threshold() {
let toml_str = r#"
model = "claude-sonnet-4-5"
max_inline_peer_notifications = -1
"#;
let profile: Profile = toml::from_str(toml_str).unwrap();
assert_eq!(profile.max_inline_peer_notifications, Some(-1));
}
#[test]
fn test_profile_toml_parses_provider_params() {
let toml_str = r#"
model = "gemini-3-pro-preview"
provider_params = { thinking_budget = 8192, top_k = 20 }
"#;
let profile: Profile = toml::from_str(toml_str).unwrap();
assert_eq!(
profile.provider_params,
Some(serde_json::json!({"thinking_budget": 8192, "top_k": 20}))
);
}
#[test]
fn profile_binding_inline_roundtrip() {
let profile = Profile {
model: "claude-opus-4-6".to_string(),
..Profile {
model: String::new(),
skills: vec![],
tools: ToolConfig::default(),
peer_description: String::new(),
external_addressable: false,
backend: None,
runtime_mode: MobRuntimeMode::AutonomousHost,
max_inline_peer_notifications: None,
output_schema: None,
provider_params: None,
}
};
let binding = ProfileBinding::Inline(profile.clone());
let json = serde_json::to_string(&binding).unwrap();
let parsed: ProfileBinding = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.as_inline().unwrap().model, "claude-opus-4-6");
}
#[test]
fn profile_binding_realm_ref_roundtrip() {
let binding = ProfileBinding::RealmRef {
realm_profile: "worker-v2".to_string(),
};
let json = serde_json::to_string(&binding).unwrap();
assert!(json.contains("realm_profile"));
let parsed: ProfileBinding = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.realm_ref_name(), Some("worker-v2"));
assert!(parsed.as_inline().is_none());
}
#[test]
fn profile_binding_backward_compat_raw_profile_deserializes_as_inline() {
let profile_json = r#"{"model":"claude-sonnet-4-5"}"#;
let binding: ProfileBinding = serde_json::from_str(profile_json).unwrap();
assert!(binding.as_inline().is_some());
assert_eq!(binding.as_inline().unwrap().model, "claude-sonnet-4-5");
}
#[test]
fn profile_binding_realm_ref_not_confused_with_inline() {
let ref_json = r#"{"realm_profile":"my-profile"}"#;
let binding: ProfileBinding = serde_json::from_str(ref_json).unwrap();
assert!(binding.realm_ref_name().is_some());
assert!(binding.as_inline().is_none());
}
#[test]
fn spawn_tooling_inherit_parent_roundtrip() {
let tooling = SpawnTooling::InheritParent {
allow_overlay: Some(vec!["shell".into()]),
deny_overlay: Some(vec!["memory_search".into()]),
};
let json = serde_json::to_string(&tooling).unwrap();
let parsed: SpawnTooling = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, tooling);
}
#[test]
fn spawn_tooling_minimal_roundtrip() {
let tooling = SpawnTooling::Minimal;
let json = serde_json::to_string(&tooling).unwrap();
let parsed: SpawnTooling = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, tooling);
}
#[test]
fn spawn_tooling_profile_realm_roundtrip() {
let tooling = SpawnTooling::Profile {
source: Box::new(ProfileSource::RealmProfile {
name: "worker-v2".into(),
}),
allow_overlay: None,
deny_overlay: Some(vec!["dangerous_tool".into()]),
};
let json = serde_json::to_string(&tooling).unwrap();
let parsed: SpawnTooling = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, tooling);
}
#[test]
fn spawn_tooling_profile_inline_roundtrip() {
let profile = Profile {
model: "claude-sonnet-4-5".into(),
skills: vec![],
tools: ToolConfig::default(),
peer_description: String::new(),
external_addressable: false,
backend: None,
runtime_mode: MobRuntimeMode::AutonomousHost,
max_inline_peer_notifications: None,
output_schema: None,
provider_params: None,
};
let tooling = SpawnTooling::Profile {
source: Box::new(ProfileSource::Inline(profile)),
allow_overlay: None,
deny_overlay: None,
};
let json = serde_json::to_string(&tooling).unwrap();
let parsed: SpawnTooling = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, tooling);
}
}