use std::path::PathBuf;
use alloy_deploy_profile::api as deploy_profile_api;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ServiceTlsMode {
Disabled,
OperatorOptional,
}
impl ServiceTlsMode {
pub fn parse(value: &str) -> Option<Self> {
match value {
"disabled" => Some(Self::Disabled),
"operator-optional" => Some(Self::OperatorOptional),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Disabled => "disabled",
Self::OperatorOptional => "operator-optional",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServiceConfig {
pub listen_address: String,
pub data_dir: PathBuf,
pub telemetry_endpoint: String,
pub tls_mode: ServiceTlsMode,
pub admin_token: Option<String>,
pub max_requests: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServiceValidationReport {
pub profile_id: &'static str,
pub compatible: bool,
pub missing_env: Vec<&'static str>,
pub missing_verbs: Vec<&'static str>,
pub missing_evidence_artifacts: Vec<&'static str>,
pub warnings: Vec<String>,
}
#[derive(Debug)]
pub enum ServiceError {
InvalidConfig(String),
Io(std::io::Error),
Driver(crate::features::client::DriverError),
}
impl From<std::io::Error> for ServiceError {
fn from(err: std::io::Error) -> Self {
Self::Io(err)
}
}
impl From<crate::features::client::DriverError> for ServiceError {
fn from(err: crate::features::client::DriverError) -> Self {
Self::Driver(err)
}
}
impl ServiceValidationReport {
pub fn to_json_pretty(&self) -> String {
let missing_env = self
.missing_env
.iter()
.map(|entry| format!("\"{}\"", entry))
.collect::<Vec<_>>()
.join(", ");
let missing_verbs = self
.missing_verbs
.iter()
.map(|entry| format!("\"{}\"", entry))
.collect::<Vec<_>>()
.join(", ");
let missing_evidence_artifacts = self
.missing_evidence_artifacts
.iter()
.map(|entry| format!("\"{}\"", entry))
.collect::<Vec<_>>()
.join(", ");
let warnings = self
.warnings
.iter()
.map(|entry| format!("\"{}\"", entry.replace('"', "\\\"")))
.collect::<Vec<_>>()
.join(", ");
format!(
concat!(
"{{\n",
" \"profile_id\": \"{}\",\n",
" \"compatible\": {},\n",
" \"missing_env\": [{}],\n",
" \"missing_verbs\": [{}],\n",
" \"missing_evidence_artifacts\": [{}],\n",
" \"warnings\": [{}]\n",
"}}\n"
),
self.profile_id,
self.compatible,
missing_env,
missing_verbs,
missing_evidence_artifacts,
warnings
)
}
}
impl ServiceConfig {
pub fn validate(&self) -> ServiceValidationReport {
let profile = deploy_profile_api::service_candidate_profile();
let available_env = ["listen_address", "data_dir", "telemetry_endpoint"];
let supported_verbs = ["inspect", "validate", "report", "start", "stop", "status"];
let available_evidence_artifacts = ["benchmark-report", "evidence-report"];
let missing_env = missing_required(profile.required_env, &available_env);
let missing_verbs = missing_required(profile.required_verbs, &supported_verbs);
let missing_evidence_artifacts = missing_required(
profile.required_evidence_artifacts,
&available_evidence_artifacts,
);
let mut warnings = Vec::new();
if matches!(self.tls_mode, ServiceTlsMode::Disabled) {
warnings.push(
"tls_mode=disabled leaves the Sprint 4 surface suitable only for trusted environments"
.to_string(),
);
}
if self.admin_token.is_none() {
warnings.push(
"admin_token is unset; admin routes are exposed without operator authentication"
.to_string(),
);
}
if self.max_requests == 0 {
warnings.push("max_requests=0 enables an unbounded serve loop".to_string());
}
ServiceValidationReport {
profile_id: profile.profile_id,
compatible: missing_env.is_empty()
&& missing_verbs.is_empty()
&& missing_evidence_artifacts.is_empty(),
missing_env,
missing_verbs,
missing_evidence_artifacts,
warnings,
}
}
}
fn missing_required(required: &[&'static str], available: &[&str]) -> Vec<&'static str> {
required
.iter()
.copied()
.filter(|entry| !available.contains(entry))
.collect()
}