use crate::llm::actions::ParameterDefinition;
use crate::llm::OllamaClient;
use crate::state::app_state::AppState;
use crate::state::ServerId;
use std::collections::HashSet;
use std::fmt;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::sync::mpsc;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum StartupParamError {
Undeclared {
key: String,
allowed: Vec<String>,
accessed_by_protocol: bool,
},
Invalid {
key: String,
detail: String,
},
}
impl fmt::Display for StartupParamError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StartupParamError::Undeclared {
key,
allowed,
accessed_by_protocol,
} => {
if *accessed_by_protocol {
write!(
f,
"Attempted to access undeclared startup parameter '{}'. Protocol must declare this parameter in get_startup_parameters(). Allowed parameters: {:?}",
key, allowed
)
} else {
write!(
f,
"Undeclared startup parameter '{}'. Protocol must declare this parameter in get_startup_parameters(). Allowed parameters: {:?}",
key, allowed
)
}
}
StartupParamError::Invalid { key, detail } => {
write!(f, "Invalid startup parameter '{}': {}", key, detail)
}
}
}
}
impl std::error::Error for StartupParamError {}
pub type StartupParamResult<T> = std::result::Result<T, StartupParamError>;
#[derive(Clone)]
pub struct StartupParams {
params: serde_json::Value,
allowed_params: HashSet<String>,
}
pub const MAX_STARTUP_PARAM_BYTES: usize = 8 * 1024 * 1024;
pub const MAX_STARTUP_PARAM_NODES: usize = 65_536;
pub const MAX_STARTUP_PARAM_DEPTH: usize = 64;
impl fmt::Debug for StartupParams {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StartupParams")
.field(
"params",
&crate::utils::redact::redact_sensitive(&self.params),
)
.field("allowed_params", &self.allowed_params)
.finish()
}
}
impl StartupParams {
pub fn preflight(params: &serde_json::Value) -> StartupParamResult<()> {
if crate::utils::json_budget::within_budget(
params,
MAX_STARTUP_PARAM_BYTES,
MAX_STARTUP_PARAM_NODES,
MAX_STARTUP_PARAM_DEPTH,
) {
Ok(())
} else {
Err(StartupParamError::Invalid {
key: "startup_params".into(),
detail: "startup_params exceeds the JSON size, node or depth budget".into(),
})
}
}
pub fn preflight_owned(params: &mut Option<serde_json::Value>) -> StartupParamResult<()> {
if let Some(value) = params.as_ref() {
if let Err(error) = Self::preflight(value) {
crate::utils::json_budget::drop_iteratively(params.take().expect("present above"));
return Err(error);
}
}
Ok(())
}
pub fn new(
params: serde_json::Value,
schema: Vec<ParameterDefinition>,
) -> StartupParamResult<Self> {
if let Err(error) = Self::preflight(¶ms) {
crate::utils::json_budget::drop_iteratively(params);
return Err(error);
}
let allowed_params: HashSet<String> = schema.iter().map(|p| p.name.clone()).collect();
let obj = params
.as_object()
.ok_or_else(|| StartupParamError::Invalid {
key: "startup_params".to_string(),
detail: "startup_params must be a JSON object".to_string(),
})?;
for key in obj.keys() {
if !allowed_params.contains(key) {
return Err(StartupParamError::Undeclared {
key: key.clone(),
allowed: sorted(&allowed_params),
accessed_by_protocol: false,
});
}
}
Ok(Self {
params,
allowed_params,
})
}
pub fn new_validated(
params: serde_json::Value,
schema: Vec<ParameterDefinition>,
) -> StartupParamResult<Self> {
let result = Self::new(params, schema.clone())?;
for parameter in &schema {
let value = result.params.get(¶meter.name);
if value.is_none_or(serde_json::Value::is_null) {
if parameter.required {
return Err(result.invalid(
¶meter.name,
"required parameter is missing or null".into(),
));
}
continue;
}
let value = value.expect("present above");
let hint = parameter.type_hint.replace(" or ", "|");
if !hint
.split('|')
.any(|kind| parameter_type_matches(value, kind.trim()))
{
return Err(result.invalid(
¶meter.name,
format!(
"expected {}, got {}",
parameter.type_hint,
diagnostic_value(¶meter.name, value)
),
));
}
}
Ok(result)
}
pub fn allowed_parameters(&self) -> Vec<String> {
sorted(&self.allowed_params)
}
pub fn get_string(&self, key: &str) -> StartupParamResult<String> {
self.validate_key(key)?;
match self.params.get(key).and_then(|v| v.as_str()) {
Some(s) => Ok(s.to_string()),
None => Err(self.invalid(
key,
format!(
"Required string parameter '{}' is missing or not a string. Params: {}",
key,
crate::utils::redact::redact_sensitive(&self.params)
),
)),
}
}
pub fn get_optional_string(&self, key: &str) -> StartupParamResult<Option<String>> {
self.validate_key(key)?;
match self.params.get(key) {
None | Some(serde_json::Value::Null) => Ok(None),
Some(v) => match v.as_str() {
Some(s) => Ok(Some(s.to_string())),
None => Err(self.invalid(
key,
format!(
"Optional string parameter '{}' exists but is not a string. Value: {}",
key,
diagnostic_value(key, v)
),
)),
},
}
}
pub fn get_bool(&self, key: &str) -> StartupParamResult<bool> {
self.validate_key(key)?;
match self.params.get(key).and_then(|v| v.as_bool()) {
Some(b) => Ok(b),
None => Err(self.invalid(
key,
format!(
"Required boolean parameter '{}' is missing or not a boolean. Params: {}",
key,
crate::utils::redact::redact_sensitive(&self.params)
),
)),
}
}
pub fn get_optional_bool(&self, key: &str) -> StartupParamResult<Option<bool>> {
self.validate_key(key)?;
match self.params.get(key) {
None | Some(serde_json::Value::Null) => Ok(None),
Some(v) => match v.as_bool() {
Some(b) => Ok(Some(b)),
None => Err(self.invalid(
key,
format!(
"Optional boolean parameter '{}' exists but is not a boolean. Value: {}",
key,
diagnostic_value(key, v)
),
)),
},
}
}
pub fn get_i64(&self, key: &str) -> StartupParamResult<i64> {
self.validate_key(key)?;
match self.params.get(key).and_then(|v| v.as_i64()) {
Some(n) => Ok(n),
None => Err(self.invalid(
key,
format!(
"Required integer parameter '{}' is missing or not an integer. Params: {}",
key,
crate::utils::redact::redact_sensitive(&self.params)
),
)),
}
}
pub fn get_optional_i64(&self, key: &str) -> StartupParamResult<Option<i64>> {
self.validate_key(key)?;
match self.params.get(key) {
None | Some(serde_json::Value::Null) => Ok(None),
Some(v) => match v.as_i64() {
Some(n) => Ok(Some(n)),
None => Err(self.invalid(
key,
format!(
"Optional integer parameter '{}' exists but is not an integer. Value: {}",
key,
diagnostic_value(key, v)
),
)),
},
}
}
pub fn get_optional_f64(&self, key: &str) -> StartupParamResult<Option<f64>> {
self.validate_key(key)?;
match self.params.get(key) {
None | Some(serde_json::Value::Null) => Ok(None),
Some(v) => match v.as_f64() {
Some(n) => Ok(Some(n)),
None => Err(self.invalid(
key,
format!(
"Optional number parameter '{}' exists but is not a number. Value: {}",
key,
diagnostic_value(key, v)
),
)),
},
}
}
pub fn get_u64(&self, key: &str) -> StartupParamResult<u64> {
self.validate_key(key)?;
match self.params.get(key).and_then(|v| v.as_u64()) {
Some(n) => Ok(n),
None => Err(self.invalid(
key,
format!(
"Required unsigned integer parameter '{}' is missing or not an unsigned integer. Params: {}",
key, crate::utils::redact::redact_sensitive(&self.params)
),
)),
}
}
pub fn get_optional_u64(&self, key: &str) -> StartupParamResult<Option<u64>> {
self.validate_key(key)?;
match self.params.get(key) {
None | Some(serde_json::Value::Null) => Ok(None),
Some(v) => match v.as_u64() {
Some(n) => Ok(Some(n)),
None => Err(self.invalid(
key,
format!(
"Optional unsigned integer parameter '{}' exists but is not an unsigned integer. Value: {}",
key, diagnostic_value(key, v)
),
)),
},
}
}
pub fn get_optional_u32(&self, key: &str) -> StartupParamResult<Option<u32>> {
self.validate_key(key)?;
match self.params.get(key) {
None | Some(serde_json::Value::Null) => Ok(None),
Some(v) => {
let val = match v.as_u64() {
Some(n) => n,
None => {
return Err(self.invalid(
key,
format!(
"Optional u32 parameter '{}' exists but is not an unsigned integer. Value: {}",
key, diagnostic_value(key, v)
),
))
}
};
if val > u32::MAX as u64 {
return Err(self.invalid(
key,
format!(
"Optional u32 parameter '{}' exceeds u32::MAX ({}). Value: {}",
key,
u32::MAX,
diagnostic_value(key, v)
),
));
}
Ok(Some(val as u32))
}
}
}
pub fn get_object(
&self,
key: &str,
) -> StartupParamResult<&serde_json::Map<String, serde_json::Value>> {
self.validate_key(key)?;
match self.params.get(key).and_then(|v| v.as_object()) {
Some(o) => Ok(o),
None => Err(self.invalid(
key,
format!(
"Required object parameter '{}' is missing or not an object. Params: {}",
key,
crate::utils::redact::redact_sensitive(&self.params)
),
)),
}
}
pub fn get_optional_object(
&self,
key: &str,
) -> StartupParamResult<Option<&serde_json::Map<String, serde_json::Value>>> {
self.validate_key(key)?;
match self.params.get(key) {
None | Some(serde_json::Value::Null) => Ok(None),
Some(v) => match v.as_object() {
Some(o) => Ok(Some(o)),
None => Err(self.invalid(
key,
format!(
"Optional object parameter '{}' exists but is not an object. Value: {}",
key,
diagnostic_value(key, v)
),
)),
},
}
}
pub fn get_array(&self, key: &str) -> StartupParamResult<&Vec<serde_json::Value>> {
self.validate_key(key)?;
match self.params.get(key).and_then(|v| v.as_array()) {
Some(a) => Ok(a),
None => Err(self.invalid(
key,
format!(
"Required array parameter '{}' is missing or not an array. Params: {}",
key,
crate::utils::redact::redact_sensitive(&self.params)
),
)),
}
}
pub fn get_optional_array(
&self,
key: &str,
) -> StartupParamResult<Option<&Vec<serde_json::Value>>> {
self.validate_key(key)?;
match self.params.get(key) {
None | Some(serde_json::Value::Null) => Ok(None),
Some(v) => match v.as_array() {
Some(a) => Ok(Some(a)),
None => Err(self.invalid(
key,
format!(
"Optional array parameter '{}' exists but is not an array. Value: {}",
key,
diagnostic_value(key, v)
),
)),
},
}
}
fn invalid(&self, key: &str, detail: String) -> StartupParamError {
StartupParamError::Invalid {
key: key.to_string(),
detail,
}
}
fn validate_key(&self, key: &str) -> StartupParamResult<()> {
if !self.allowed_params.contains(key) {
return Err(StartupParamError::Undeclared {
key: key.to_string(),
allowed: sorted(&self.allowed_params),
accessed_by_protocol: true,
});
}
Ok(())
}
}
fn diagnostic_value(key: &str, value: &serde_json::Value) -> serde_json::Value {
if crate::utils::redact::is_shown_redacted(key, value) {
serde_json::Value::String(crate::utils::redact::REDACTED.into())
} else {
crate::utils::redact::redact_sensitive(value)
}
}
fn sorted(set: &HashSet<String>) -> Vec<String> {
let mut v: Vec<String> = set.iter().cloned().collect();
v.sort();
v
}
#[derive(Clone)]
pub struct SpawnContext {
#[deprecated(since = "1.0.0", note = "Use host/port fields instead")]
pub listen_addr: SocketAddr,
pub mac_address: Option<String>,
pub interface: Option<String>,
pub host: Option<String>,
pub port: Option<u16>,
pub llm_client: OllamaClient,
pub state: Arc<AppState>,
pub status_tx: mpsc::UnboundedSender<String>,
pub server_id: ServerId,
pub startup_params: Option<StartupParams>,
}
impl SpawnContext {
pub fn socket_addr(&self) -> Option<SocketAddr> {
match (&self.host, self.port) {
(Some(host), Some(port)) => host
.parse::<std::net::IpAddr>()
.ok()
.map(|ip| SocketAddr::new(ip, port)),
_ => None,
}
}
pub fn interface(&self) -> Option<&str> {
self.interface.as_deref()
}
pub fn mac_address(&self) -> Option<&str> {
self.mac_address.as_deref()
}
#[allow(deprecated)]
pub fn legacy_listen_addr(&self) -> SocketAddr {
self.listen_addr
}
}
fn parameter_type_matches(value: &serde_json::Value, kind: &str) -> bool {
match kind {
"string" => value.is_string(),
"number" => value.is_number(),
"integer" => value.is_i64() || value.is_u64(),
"boolean" => value.is_boolean(),
"object" => value.is_object(),
"array" => value.is_array(),
"null" => value.is_null(),
"array of strings" => value
.as_array()
.is_some_and(|items| items.iter().all(|item| item.is_string())),
"array of objects" => value
.as_array()
.is_some_and(|items| items.iter().all(|item| item.is_object())),
"array of integers" => value
.as_array()
.is_some_and(|items| items.iter().all(|item| item.is_i64() || item.is_u64())),
"array of booleans" => value
.as_array()
.is_some_and(|items| items.iter().all(|item| item.is_boolean())),
_ => true,
}
}