use serde::{Deserialize, Serialize};
use crate::address::Address;
use crate::consts_policy::CLOCK_STAMP_DEFAULT;
use crate::error::{AxCode, AxError};
use crate::model::Effort;
use crate::tool::ServerLabel;
mod interpreter;
pub use interpreter::Interpreter;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct EnvVarName(String);
impl EnvVarName {
pub fn parse(raw: &str) -> Result<EnvVarName, AxError> {
let reject = |violation: &str| {
Err(AxError::failure(
AxCode::InvalidArgs,
"declare an environment variable",
raw.to_owned(),
)
.with_recovery(format!(
"{violation}; declare only the names a build needs, and keep a credential in \
the vault as a `secret:realm/name` reference"
)))
};
if raw.is_empty() {
return reject("the name is empty");
}
if raw.contains('=') {
return reject("the name carries `=`, which separates a name from its value");
}
if raw.chars().any(char::is_control) {
return reject("the name carries a control character");
}
if crate::secret::names_a_credential(raw) {
return reject("the name reads as a credential");
}
Ok(EnvVarName(raw.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for EnvVarName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl TryFrom<String> for EnvVarName {
type Error = AxError;
fn try_from(raw: String) -> Result<EnvVarName, AxError> {
EnvVarName::parse(&raw)
}
}
impl From<EnvVarName> for String {
fn from(name: EnvVarName) -> String {
name.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ClockStampGranularity {
Off,
Minute,
FiveMinute,
Hour,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "u64", into = "u64")]
pub struct SecondThreshold(u64);
impl SecondThreshold {
pub fn parse(raw: u64) -> Result<SecondThreshold, AxError> {
let floor = crate::consts_policy::CTX_REMINDER_SECOND_MIN;
let ceiling = crate::consts_policy::CTX_REMINDER_SECOND_MAX;
if (floor..=ceiling).contains(&raw) {
return Ok(SecondThreshold(raw));
}
Err(AxError::failure(
AxCode::InvalidArgs,
"set the second context-reminder threshold",
raw.to_string(),
)
.with_recovery(format!(
"the legal domain is {floor} through {ceiling} percent of the window; state a whole \
percent in it, or leave the key out for {}",
crate::consts_policy::CTX_REMINDER_SECOND_DEFAULT
)))
}
#[must_use]
pub fn percent(self) -> u64 {
self.0
}
}
impl TryFrom<u64> for SecondThreshold {
type Error = AxError;
fn try_from(raw: u64) -> Result<SecondThreshold, AxError> {
SecondThreshold::parse(raw)
}
}
impl From<SecondThreshold> for u64 {
fn from(rung: SecondThreshold) -> u64 {
rung.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LayeredValue<T> {
pub city: Option<T>,
pub building: Option<T>,
pub resident: Option<T>,
}
impl<T> Default for LayeredValue<T> {
fn default() -> Self {
LayeredValue {
city: None,
building: None,
resident: None,
}
}
}
impl<T> LayeredValue<T> {
pub fn resolve(&self) -> Option<&T> {
self.resident
.as_ref()
.or(self.building.as_ref())
.or(self.city.as_ref())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClockZone {
pub id: String,
pub offset_min: i32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SandboxLimits {
pub shell: bool,
#[serde(default)]
pub interpreter: Interpreter,
pub fuel: u64,
pub mounts: Vec<Address>,
#[serde(default)]
pub env_passthrough: Vec<EnvVarName>,
#[serde(default)]
pub trusted: Vec<ServerLabel>,
}
impl SandboxLimits {
#[must_use]
pub fn trusts(&self, label: &ServerLabel) -> bool {
self.trusted.iter().any(|allowed| allowed == label)
}
}
impl Default for SandboxLimits {
fn default() -> Self {
SandboxLimits {
shell: false,
interpreter: Interpreter::System,
fuel: crate::consts_policy::SANDBOX_FUEL_DEFAULT,
mounts: Vec::new(),
env_passthrough: Vec::new(),
trusted: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct McpServer {
pub label: ServerLabel,
pub transport: McpTransport,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum McpTransport {
Stdio {
command: String,
args: Vec<String>,
env: Vec<(String, String)>,
},
Http {
url: String,
headers: Vec<(String, String)>,
},
Sse {
url: String,
headers: Vec<(String, String)>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrozenConfig {
pub clock_stamp: ClockStampGranularity,
pub clock_zones: Vec<ClockZone>,
pub sandbox: SandboxLimits,
pub mcp: Vec<McpServer>,
pub effort: Option<Effort>,
pub second_threshold: Option<SecondThreshold>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct LiveConfig {}
pub fn freeze(
clock_stamp: &LayeredValue<ClockStampGranularity>,
clock_zones: &LayeredValue<Vec<ClockZone>>,
effort: &LayeredValue<Effort>,
sandbox: &LayeredValue<SandboxLimits>,
mcp: &LayeredValue<Vec<McpServer>>,
second_threshold: &LayeredValue<SecondThreshold>,
) -> FrozenConfig {
FrozenConfig {
clock_stamp: *clock_stamp.resolve().unwrap_or(&CLOCK_STAMP_DEFAULT),
clock_zones: clock_zones.resolve().cloned().unwrap_or_default(),
sandbox: sandbox.resolve().cloned().unwrap_or_default(),
effort: effort.resolve().copied(),
mcp: mcp.resolve().cloned().unwrap_or_default(),
second_threshold: second_threshold.resolve().copied(),
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
reason = "test code"
)]
mod tests;