use axum::{
body::Body,
debug_handler,
extract::{Path, State},
http::{Request, StatusCode},
response::{IntoResponse, Response},
Json,
};
use microsandbox_core::management::{menv, orchestra};
use microsandbox_utils::{DEFAULT_CONFIG, DEFAULT_PORTAL_GUEST_PORT, MICROSANDBOX_CONFIG_FILENAME};
use reqwest;
use serde_json::{self, json};
use serde_yaml;
use std::path::PathBuf;
use tokio::{
fs as tokio_fs,
time::{sleep, timeout, Duration},
};
use tracing::{debug, trace, warn};
use crate::{
error::ServerError,
mcp, middleware,
payload::{
JsonRpcError, JsonRpcRequest, JsonRpcResponse, JsonRpcResponseOrNotification,
RegularMessageResponse, SandboxMetricsGetParams, SandboxStartParams, SandboxStopParams,
JSONRPC_VERSION,
},
state::AppState,
SandboxStatus, SandboxStatusResponse, ServerResult,
};
pub async fn health() -> ServerResult<impl IntoResponse> {
Ok((
StatusCode::OK,
Json(RegularMessageResponse {
message: "Service is healthy".to_string(),
}),
))
}
#[debug_handler]
pub async fn mcp_handler(
State(state): State<AppState>,
Json(request): Json<JsonRpcRequest>,
) -> ServerResult<impl IntoResponse> {
debug!(?request, "Received MCP request");
if request.jsonrpc != JSONRPC_VERSION {
let error = JsonRpcError {
code: -32600,
message: "Invalid or missing jsonrpc version field".to_string(),
data: None,
};
return Ok(JsonRpcResponseOrNotification::error(
error,
request.id.clone(),
));
}
let request_id = request.id.clone();
match mcp::handle_mcp_method(state, request).await {
Ok(response) => {
Ok(response)
}
Err(e) => {
let error = JsonRpcError {
code: -32603,
message: format!("MCP method error: {}", e),
data: None,
};
Ok(JsonRpcResponseOrNotification::error(error, request_id))
}
}
}
#[debug_handler]
pub async fn json_rpc_handler(
State(state): State<AppState>,
Json(request): Json<JsonRpcRequest>,
) -> ServerResult<impl IntoResponse> {
debug!(?request, "Received JSON-RPC request");
if request.jsonrpc != JSONRPC_VERSION {
let error = JsonRpcError {
code: -32600,
message: "Invalid or missing jsonrpc version field".to_string(),
data: None,
};
return Ok((
StatusCode::BAD_REQUEST,
Json(JsonRpcResponse::error(error, request.id.clone())),
));
}
let method = request.method.as_str();
let id = request.id.clone();
match method {
"sandbox.start" => {
let start_params: SandboxStartParams =
serde_json::from_value(request.params.clone()).map_err(|e| {
ServerError::ValidationError(crate::error::ValidationError::InvalidInput(
format!("Invalid params for sandbox.start: {}", e),
))
})?;
let result = sandbox_start_impl(state, start_params).await?;
Ok((
StatusCode::OK,
Json(JsonRpcResponse::success(json!(result), id)),
))
}
"sandbox.stop" => {
let stop_params: SandboxStopParams = serde_json::from_value(request.params.clone())
.map_err(|e| {
ServerError::ValidationError(crate::error::ValidationError::InvalidInput(
format!("Invalid params for sandbox.stop: {}", e),
))
})?;
let result = sandbox_stop_impl(state, stop_params).await?;
Ok((
StatusCode::OK,
Json(JsonRpcResponse::success(json!(result), id)),
))
}
"sandbox.metrics.get" => {
let metrics_params: SandboxMetricsGetParams =
serde_json::from_value(request.params.clone()).map_err(|e| {
ServerError::ValidationError(crate::error::ValidationError::InvalidInput(
format!("Invalid params for sandbox.metrics.get: {}", e),
))
})?;
let result = sandbox_get_metrics_impl(state.clone(), metrics_params).await?;
Ok((
StatusCode::OK,
Json(JsonRpcResponse::success(json!(result), id)),
))
}
"sandbox.repl.run" | "sandbox.command.run" => {
match forward_rpc_to_portal(state, request).await {
Ok((status, json_response)) => Ok((status, json_response)),
Err(e) => Err(e),
}
}
_ => {
let error = JsonRpcError {
code: -32601,
message: format!("Method not found: {}", method),
data: None,
};
Ok((
StatusCode::NOT_FOUND,
Json(JsonRpcResponse::error(error, id)),
))
}
}
}
pub async fn forward_rpc_to_portal(
state: AppState,
request: JsonRpcRequest,
) -> ServerResult<(StatusCode, Json<JsonRpcResponse>)> {
let (sandbox_name, namespace) = if let Some(params) = request.params.as_object() {
let sandbox = params
.get("sandbox")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ServerError::ValidationError(crate::error::ValidationError::InvalidInput(
"Missing required 'sandbox' parameter for portal request".to_string(),
))
})?;
let namespace = params
.get("namespace")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ServerError::ValidationError(crate::error::ValidationError::InvalidInput(
"Missing required 'namespace' parameter for portal request".to_string(),
))
})?;
(sandbox, namespace)
} else {
return Err(ServerError::ValidationError(
crate::error::ValidationError::InvalidInput(
"Request parameters must be an object containing 'sandbox' and 'namespace'"
.to_string(),
),
));
};
let portal_url = state
.get_portal_url_for_sandbox(namespace, sandbox_name)
.await?;
let portal_rpc_url = format!("{}/api/v1/rpc", portal_url);
debug!("Forwarding RPC to portal: {}", portal_rpc_url);
let client = reqwest::Client::new();
const MAX_RETRIES: u32 = 10_000;
const TIMEOUT_MS: u64 = 50;
let mut retry_count = 0;
let mut last_error = None;
while retry_count < MAX_RETRIES {
match client
.head(&portal_url)
.timeout(Duration::from_millis(TIMEOUT_MS))
.send()
.await
{
Ok(response) => {
debug!(
"Successfully connected to portal after {} retries (status: {})",
retry_count,
response.status()
);
break;
}
Err(e) => {
last_error = Some(e);
trace!("Connection attempt {} failed, retrying...", retry_count + 1);
}
}
retry_count += 1;
}
if retry_count >= MAX_RETRIES {
let error_msg = if let Some(e) = last_error {
format!(
"Failed to connect to portal after {} retries: {}",
MAX_RETRIES, e
)
} else {
format!("Failed to connect to portal after {} retries", MAX_RETRIES)
};
return Err(ServerError::InternalError(error_msg));
}
let response = client
.post(&portal_rpc_url)
.json(&request)
.send()
.await
.map_err(|e| {
ServerError::InternalError(format!("Failed to forward RPC to portal: {}", e))
})?;
if !response.status().is_success() {
let status = response.status();
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(ServerError::InternalError(format!(
"Portal returned error status {}: {}",
status, error_text
)));
}
let portal_response: JsonRpcResponse = response.json().await.map_err(|e| {
ServerError::InternalError(format!("Failed to parse portal response: {}", e))
})?;
Ok((StatusCode::OK, Json(portal_response)))
}
pub async fn sandbox_start_impl(
state: AppState,
params: SandboxStartParams,
) -> ServerResult<String> {
validate_sandbox_name(¶ms.sandbox)?;
validate_namespace(¶ms.namespace)?;
let namespace_dir = state
.get_config()
.get_namespace_dir()
.join(¶ms.namespace);
let config_file = MICROSANDBOX_CONFIG_FILENAME;
let config_path = namespace_dir.join(config_file);
let sandbox = ¶ms.sandbox;
if !namespace_dir.exists() {
tokio_fs::create_dir_all(&namespace_dir)
.await
.map_err(|e| {
ServerError::InternalError(format!("Failed to create namespace directory: {}", e))
})?;
menv::initialize(Some(namespace_dir.clone()))
.await
.map_err(|e| {
ServerError::InternalError(format!(
"Failed to initialize microsandbox environment: {}",
e
))
})?;
}
let has_config_in_request = params
.config
.as_ref()
.and_then(|c| c.image.as_ref())
.is_some();
let has_existing_config_file = config_path.exists();
if !has_config_in_request && !has_existing_config_file {
return Err(ServerError::ValidationError(
crate::error::ValidationError::InvalidInput(format!(
"No configuration provided and no existing configuration found for sandbox '{}'",
sandbox
)),
));
}
let mut config_yaml: serde_yaml::Value;
if has_existing_config_file {
let config_content = tokio_fs::read_to_string(&config_path).await.map_err(|e| {
ServerError::InternalError(format!("Failed to read config file: {}", e))
})?;
config_yaml = serde_yaml::from_str(&config_content).map_err(|e| {
ServerError::InternalError(format!("Failed to parse config file: {}", e))
})?;
if !has_config_in_request {
let has_sandbox_config = config_yaml
.get("sandboxes")
.and_then(|sandboxes| sandboxes.get(sandbox))
.is_some();
if !has_sandbox_config {
return Err(ServerError::ValidationError(
crate::error::ValidationError::InvalidInput(format!(
"Sandbox '{}' not found in existing configuration",
sandbox
)),
));
}
}
} else {
if !has_config_in_request {
return Err(ServerError::ValidationError(
crate::error::ValidationError::InvalidInput(
"No configuration provided and no existing configuration file".to_string(),
),
));
}
tokio_fs::write(&config_path, DEFAULT_CONFIG)
.await
.map_err(|e| {
ServerError::InternalError(format!("Failed to create config file: {}", e))
})?;
config_yaml = serde_yaml::from_str(DEFAULT_CONFIG).map_err(|e| {
ServerError::InternalError(format!("Failed to parse default config: {}", e))
})?;
}
if !config_yaml.is_mapping() {
config_yaml = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
}
let config_map = config_yaml.as_mapping_mut().unwrap();
if !config_map.contains_key(&serde_yaml::Value::String("sandboxes".to_string())) {
config_map.insert(
serde_yaml::Value::String("sandboxes".to_string()),
serde_yaml::Value::Mapping(serde_yaml::Mapping::new()),
);
}
let sandboxes_key = serde_yaml::Value::String("sandboxes".to_string());
let sandboxes_value = config_map.get_mut(&sandboxes_key).unwrap();
if !sandboxes_value.is_mapping() {
*sandboxes_value = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
}
let sandboxes_map = sandboxes_value.as_mapping_mut().unwrap();
if let Some(config) = ¶ms.config {
if config.image.is_some() {
let mut sandbox_map = serde_yaml::Mapping::new();
if let Some(image) = &config.image {
sandbox_map.insert(
serde_yaml::Value::String("image".to_string()),
serde_yaml::Value::String(image.clone()),
);
}
if let Some(memory) = config.memory {
sandbox_map.insert(
serde_yaml::Value::String("memory".to_string()),
serde_yaml::Value::Number(serde_yaml::Number::from(memory)),
);
}
if let Some(cpus) = config.cpus {
sandbox_map.insert(
serde_yaml::Value::String("cpus".to_string()),
serde_yaml::Value::Number(serde_yaml::Number::from(cpus)),
);
}
if !config.volumes.is_empty() {
let volumes_array = config
.volumes
.iter()
.map(|v| serde_yaml::Value::String(v.clone()))
.collect::<Vec<_>>();
sandbox_map.insert(
serde_yaml::Value::String("volumes".to_string()),
serde_yaml::Value::Sequence(volumes_array),
);
}
if !config.ports.is_empty() {
let ports_array = config
.ports
.iter()
.map(|p| serde_yaml::Value::String(p.clone()))
.collect::<Vec<_>>();
sandbox_map.insert(
serde_yaml::Value::String("ports".to_string()),
serde_yaml::Value::Sequence(ports_array),
);
}
if !config.envs.is_empty() {
let envs_array = config
.envs
.iter()
.map(|e| serde_yaml::Value::String(e.clone()))
.collect::<Vec<_>>();
sandbox_map.insert(
serde_yaml::Value::String("envs".to_string()),
serde_yaml::Value::Sequence(envs_array),
);
}
if !config.depends_on.is_empty() {
let depends_on_array = config
.depends_on
.iter()
.map(|d| serde_yaml::Value::String(d.clone()))
.collect::<Vec<_>>();
sandbox_map.insert(
serde_yaml::Value::String("depends_on".to_string()),
serde_yaml::Value::Sequence(depends_on_array),
);
}
if let Some(workdir) = &config.workdir {
sandbox_map.insert(
serde_yaml::Value::String("workdir".to_string()),
serde_yaml::Value::String(workdir.clone()),
);
}
if let Some(shell) = &config.shell {
sandbox_map.insert(
serde_yaml::Value::String("shell".to_string()),
serde_yaml::Value::String(shell.clone()),
);
}
if !config.scripts.is_empty() {
let mut scripts_map = serde_yaml::Mapping::new();
for (script_name, script) in &config.scripts {
scripts_map.insert(
serde_yaml::Value::String(script_name.clone()),
serde_yaml::Value::String(script.clone()),
);
}
sandbox_map.insert(
serde_yaml::Value::String("scripts".to_string()),
serde_yaml::Value::Mapping(scripts_map),
);
}
if let Some(exec) = &config.exec {
sandbox_map.insert(
serde_yaml::Value::String("exec".to_string()),
serde_yaml::Value::String(exec.clone()),
);
}
sandboxes_map.insert(
serde_yaml::Value::String(sandbox.clone()),
serde_yaml::Value::Mapping(sandbox_map),
);
}
}
let sandbox_key = format!("{}/{}", params.namespace, params.sandbox);
let port = {
let mut port_manager = state.get_port_manager().write().await;
port_manager.assign_port(&sandbox_key).await.map_err(|e| {
ServerError::InternalError(format!("Failed to assign portal port: {}", e))
})?
};
debug!("Assigned portal port {} to sandbox {}", port, sandbox_key);
let sandbox_config = sandboxes_map
.get_mut(&serde_yaml::Value::String(sandbox.clone()))
.ok_or_else(|| {
ServerError::InternalError(format!("Sandbox '{}' not found in configuration", sandbox))
})?
.as_mapping_mut()
.ok_or_else(|| {
ServerError::InternalError(format!(
"Sandbox '{}' configuration is not a mapping",
sandbox
))
})?;
let guest_port = DEFAULT_PORTAL_GUEST_PORT;
let portal_port_mapping = format!("{}:{}", port, guest_port);
let ports_key = serde_yaml::Value::String("ports".to_string());
if let Some(ports) = sandbox_config.get_mut(&ports_key) {
if let Some(ports_seq) = ports.as_sequence_mut() {
ports_seq.retain(|p| {
p.as_str()
.map(|s| !s.ends_with(&format!(":{}", guest_port)))
.unwrap_or(true)
});
ports_seq.push(serde_yaml::Value::String(portal_port_mapping));
}
} else {
let mut ports_seq = serde_yaml::Sequence::new();
ports_seq.push(serde_yaml::Value::String(portal_port_mapping));
sandbox_config.insert(ports_key, serde_yaml::Value::Sequence(ports_seq));
}
let updated_config = serde_yaml::to_string(&config_yaml)
.map_err(|e| ServerError::InternalError(format!("Failed to serialize config: {}", e)))?;
tokio_fs::write(&config_path, updated_config)
.await
.map_err(|e| ServerError::InternalError(format!("Failed to write config file: {}", e)))?;
orchestra::up(
vec![sandbox.clone()],
Some(&namespace_dir),
Some(config_file),
true,
)
.await
.map_err(|e| {
ServerError::InternalError(format!("Failed to start sandbox {}: {}", params.sandbox, e))
})?;
let potentially_first_time_pull = if let Some(config) = ¶ms.config {
config.image.is_some()
} else {
false
};
let poll_timeout = if potentially_first_time_pull {
Duration::from_secs(180) } else {
Duration::from_secs(60) };
debug!("Waiting for sandbox {} to start...", sandbox);
match timeout(
poll_timeout,
poll_sandbox_until_running(¶ms.sandbox, &namespace_dir, config_file),
)
.await
{
Ok(result) => match result {
Ok(_) => {
debug!("Sandbox {} is now running", sandbox);
Ok(format!("Sandbox {} started successfully", params.sandbox))
}
Err(e) => {
warn!("Failed to verify sandbox {} is running: {}", sandbox, e);
Ok(format!(
"Sandbox {} was started, but couldn't verify it's running: {}",
params.sandbox, e
))
}
},
Err(_) => {
warn!("Timeout waiting for sandbox {} to start", sandbox);
Ok(format!(
"Sandbox {} was started, but timed out waiting for it to be fully running. It may still be initializing.",
params.sandbox
))
}
}
}
async fn poll_sandbox_until_running(
sandbox_name: &str,
namespace_dir: &PathBuf,
config_file: &str,
) -> ServerResult<()> {
const POLL_INTERVAL: Duration = Duration::from_millis(20);
const MAX_ATTEMPTS: usize = 2500;
for attempt in 1..=MAX_ATTEMPTS {
let statuses = orchestra::status(
vec![sandbox_name.to_string()],
Some(namespace_dir),
Some(config_file),
)
.await
.map_err(|e| ServerError::InternalError(format!("Failed to get sandbox status: {}", e)))?;
if let Some(status) = statuses.iter().find(|s| s.name == sandbox_name) {
if status.running {
debug!(
"Sandbox {} is running (verified on attempt {})",
sandbox_name, attempt
);
return Ok(());
}
}
sleep(POLL_INTERVAL).await;
}
Err(ServerError::InternalError(format!(
"Exceeded maximum attempts to verify sandbox {} is running",
sandbox_name
)))
}
pub async fn sandbox_stop_impl(state: AppState, params: SandboxStopParams) -> ServerResult<String> {
validate_sandbox_name(¶ms.sandbox)?;
validate_namespace(¶ms.namespace)?;
let namespace_dir = state
.get_config()
.get_namespace_dir()
.join(¶ms.namespace);
let config_file = MICROSANDBOX_CONFIG_FILENAME;
let sandbox = ¶ms.sandbox;
let sandbox_key = format!("{}/{}", params.namespace, params.sandbox);
if !namespace_dir.exists() {
return Err(ServerError::ValidationError(
crate::error::ValidationError::InvalidInput(format!(
"Namespace directory '{}' does not exist",
params.namespace
)),
));
}
let config_path = namespace_dir.join(config_file);
if !config_path.exists() {
return Err(ServerError::ValidationError(
crate::error::ValidationError::InvalidInput(format!(
"Configuration file not found for namespace '{}'",
params.namespace
)),
));
}
orchestra::down(
vec![sandbox.clone()],
Some(&namespace_dir),
Some(config_file),
)
.await
.map_err(|e| {
ServerError::InternalError(format!("Failed to stop sandbox {}: {}", params.sandbox, e))
})?;
{
let mut port_manager = state.get_port_manager().write().await;
port_manager.release_port(&sandbox_key).await.map_err(|e| {
ServerError::InternalError(format!("Failed to release portal port: {}", e))
})?;
}
debug!("Released portal port for sandbox {}", sandbox_key);
Ok(format!("Sandbox {} stopped successfully", params.sandbox))
}
pub async fn sandbox_get_metrics_impl(
state: AppState,
params: SandboxMetricsGetParams,
) -> ServerResult<SandboxStatusResponse> {
if params.namespace != "*" {
validate_namespace(¶ms.namespace)?;
}
if let Some(sandbox) = ¶ms.sandbox {
validate_sandbox_name(sandbox)?;
}
let namespaces_dir = state.get_config().get_namespace_dir();
if !namespaces_dir.exists() {
return Err(ServerError::InternalError(format!(
"Namespaces directory '{}' does not exist",
namespaces_dir.display()
)));
}
let mut all_statuses = Vec::new();
if params.namespace == "*" {
let mut entries = tokio::fs::read_dir(&namespaces_dir).await.map_err(|e| {
ServerError::InternalError(format!("Failed to read namespaces directory: {}", e))
})?;
while let Some(entry) = entries.next_entry().await.map_err(|e| {
ServerError::InternalError(format!("Failed to read namespace directory entry: {}", e))
})? {
let path = entry.path();
if !path.is_dir() {
continue;
}
let namespace = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
let sandbox_names = if let Some(sandbox) = ¶ms.sandbox {
vec![sandbox.clone()]
} else {
vec![]
};
match orchestra::status(sandbox_names, Some(&path), None).await {
Ok(statuses) => {
for status in statuses {
all_statuses.push(SandboxStatus {
namespace: namespace.clone(),
name: status.name,
running: status.running,
cpu_usage: status.cpu_usage,
memory_usage: status.memory_usage,
disk_usage: status.disk_usage,
});
}
}
Err(e) => {
tracing::warn!("Error getting metrics for namespace {}: {}", namespace, e);
}
}
}
} else {
let namespace_dir = namespaces_dir.join(¶ms.namespace);
if !namespace_dir.exists() {
return Err(ServerError::ValidationError(
crate::error::ValidationError::InvalidInput(format!(
"Namespace directory '{}' does not exist",
params.namespace
)),
));
}
let sandbox_names = if let Some(sandbox) = ¶ms.sandbox {
vec![sandbox.clone()]
} else {
vec![]
};
match orchestra::status(sandbox_names, Some(&namespace_dir), None).await {
Ok(statuses) => {
for status in statuses {
all_statuses.push(SandboxStatus {
namespace: params.namespace.clone(),
name: status.name,
running: status.running,
cpu_usage: status.cpu_usage,
memory_usage: status.memory_usage,
disk_usage: status.disk_usage,
});
}
}
Err(e) => {
return Err(ServerError::InternalError(format!(
"Error getting metrics for namespace {}: {}",
params.namespace, e
)));
}
}
}
Ok(SandboxStatusResponse {
sandboxes: all_statuses,
})
}
pub async fn proxy_request(
State(_state): State<AppState>,
Path((namespace, sandbox, path)): Path<(String, String, PathBuf)>,
req: Request<Body>,
) -> ServerResult<impl IntoResponse> {
let path_str = path.display().to_string();
let original_uri = req.uri().clone();
let _target_uri = middleware::proxy_uri(original_uri, &namespace, &sandbox);
let response = format!(
"Axum Proxy Request\n\nNamespace: {}\nSandbox: {}\nPath: {}\nMethod: {}\nHeaders: {:?}",
namespace,
sandbox,
path_str,
req.method(),
req.headers()
);
let result = Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/plain")
.body(Body::from(response))
.unwrap();
Ok(result)
}
pub async fn proxy_fallback() -> ServerResult<impl IntoResponse> {
Ok((StatusCode::NOT_FOUND, "Resource not found"))
}
fn validate_sandbox_name(name: &str) -> ServerResult<()> {
if name.is_empty() {
return Err(ServerError::ValidationError(
crate::error::ValidationError::InvalidInput("Sandbox name cannot be empty".to_string()),
));
}
if name.len() > 63 {
return Err(ServerError::ValidationError(
crate::error::ValidationError::InvalidInput(
"Sandbox name cannot exceed 63 characters".to_string(),
),
));
}
let valid_chars = name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
if !valid_chars {
return Err(ServerError::ValidationError(
crate::error::ValidationError::InvalidInput(
"Sandbox name can only contain alphanumeric characters, hyphens, or underscores"
.to_string(),
),
));
}
if !name.chars().next().unwrap().is_ascii_alphanumeric() {
return Err(ServerError::ValidationError(
crate::error::ValidationError::InvalidInput(
"Sandbox name must start with an alphanumeric character".to_string(),
),
));
}
Ok(())
}
fn validate_namespace(namespace: &str) -> ServerResult<()> {
if namespace.is_empty() {
return Err(ServerError::ValidationError(
crate::error::ValidationError::InvalidInput("Namespace cannot be empty".to_string()),
));
}
if namespace.len() > 63 {
return Err(ServerError::ValidationError(
crate::error::ValidationError::InvalidInput(
"Namespace cannot exceed 63 characters".to_string(),
),
));
}
if namespace == "*" {
return Err(ServerError::ValidationError(
crate::error::ValidationError::InvalidInput(
"Wildcard namespace (*) is not valid for sandbox creation".to_string(),
),
));
}
let valid_chars = namespace
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
if !valid_chars {
return Err(ServerError::ValidationError(
crate::error::ValidationError::InvalidInput(
"Namespace can only contain alphanumeric characters, hyphens, or underscores"
.to_string(),
),
));
}
if !namespace.chars().next().unwrap().is_ascii_alphanumeric() {
return Err(ServerError::ValidationError(
crate::error::ValidationError::InvalidInput(
"Namespace must start with an alphanumeric character".to_string(),
),
));
}
Ok(())
}