use std::{fmt, fs, path::Path};
use serde::Deserialize;
use thiserror::Error;
#[derive(Clone, Default, Deserialize)]
#[serde(transparent)]
pub struct SecretString(String);
impl SecretString {
pub fn expose(&self) -> &str {
&self.0
}
fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl fmt::Debug for SecretString {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("[REDACTED]")
}
}
#[derive(Debug, Clone, Copy, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeMode {
#[default]
Local,
Production,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct RuntimeConfig {
#[serde(default)]
pub mode: RuntimeMode,
#[serde(default)]
pub server: ServerConfig,
#[serde(default)]
pub scheduler: SchedulerConfig,
#[serde(default)]
pub agent_infra: AgentInfraConfig,
#[serde(default)]
pub journal: JournalConfig,
#[serde(default)]
pub auth: AuthConfig,
pub binding: AgentBindingConfig,
#[serde(default)]
pub context: ContextConfig,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
pub struct ServerConfig {
pub bind: String,
pub shutdown_grace_seconds: u64,
pub transport: TransportConfig,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
bind: "127.0.0.1:8080".into(),
shutdown_grace_seconds: 30,
transport: TransportConfig::default(),
}
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
pub struct TransportConfig {
pub agent_tasks: bool,
pub openai_responses: bool,
pub claude_messages: bool,
pub sse: bool,
pub websocket: bool,
pub websocket_max_message_bytes: usize,
}
impl Default for TransportConfig {
fn default() -> Self {
Self {
agent_tasks: true,
openai_responses: true,
claude_messages: true,
sse: true,
websocket: true,
websocket_max_message_bytes: 256 * 1024,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
pub struct SchedulerConfig {
pub max_concurrent_executions: usize,
}
impl Default for SchedulerConfig {
fn default() -> Self {
Self {
max_concurrent_executions: 16,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
pub struct AgentInfraConfig {
pub gateway_url: String,
pub request_timeout_ms: u64,
pub max_response_bytes: usize,
pub trusted_mesh_http: bool,
pub workload_subject: String,
#[serde(default)]
pub workload_token: SecretString,
pub delegation_ttl_seconds: u64,
}
impl Default for AgentInfraConfig {
fn default() -> Self {
Self {
gateway_url: "http://127.0.0.1:5100".into(),
request_timeout_ms: 30_000,
max_response_bytes: 16 * 1024 * 1024,
trusted_mesh_http: true,
workload_subject: "local-anonymous".into(),
workload_token: SecretString::default(),
delegation_ttl_seconds: 300,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(
tag = "type",
rename_all = "snake_case",
rename_all_fields = "snake_case",
deny_unknown_fields
)]
pub enum JournalConfig {
Memory { max_events_per_execution: usize },
}
impl Default for JournalConfig {
fn default() -> Self {
Self::Memory {
max_events_per_execution: 10_000,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(
tag = "type",
rename_all = "snake_case",
rename_all_fields = "snake_case",
deny_unknown_fields
)]
pub enum AuthConfig {
LocalStatic {
#[serde(default)]
token: SecretString,
subject: String,
tenant_id: String,
project_id: String,
#[serde(default)]
capabilities: Vec<String>,
},
TrustedGateway {
#[serde(default)]
shared_secret: SecretString,
},
}
impl Default for AuthConfig {
fn default() -> Self {
Self::LocalStatic {
token: SecretString::default(),
subject: "local-user".into(),
tenant_id: "local-tenant".into(),
project_id: "local-project".into(),
capabilities: vec![
"*".into(),
"files:read".into(),
"files:write".into(),
"commands:execute".into(),
"interaction:request".into(),
"agent:delegate".into(),
],
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct AgentBindingConfig {
pub runtime_id: String,
pub agent_id: String,
pub model: String,
pub workspace_id: String,
#[serde(default)]
pub definition_path: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
pub struct ContextConfig {
pub max_messages: usize,
pub max_input_tokens: u64,
pub reserved_output_tokens: u32,
pub protocol_overhead_tokens: u64,
pub auto_compact: bool,
pub compaction: ContextCompactionConfig,
}
impl Default for ContextConfig {
fn default() -> Self {
Self {
max_messages: 128,
max_input_tokens: 128 * 1024,
reserved_output_tokens: 8 * 1024,
protocol_overhead_tokens: 256,
auto_compact: true,
compaction: ContextCompactionConfig::default(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
pub struct ContextCompactionConfig {
pub trigger_percent: u8,
pub target_percent: u8,
pub emergency_percent: u8,
pub min_reclaim_percent: u8,
pub min_turns_between: usize,
pub checkpoint_max_chars: usize,
}
impl Default for ContextCompactionConfig {
fn default() -> Self {
Self {
trigger_percent: 82,
target_percent: 60,
emergency_percent: 95,
min_reclaim_percent: 10,
min_turns_between: 4,
checkpoint_max_chars: 8_000,
}
}
}
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("failed to read settings file: {0}")]
Read(#[from] std::io::Error),
#[error("config TOML is invalid: {0}")]
TomlDecode(#[from] toml::de::Error),
#[error("settings are invalid: {0}")]
Invalid(String),
}
impl RuntimeConfig {
pub fn load(path: Option<&Path>) -> Result<Self, ConfigError> {
let mut config = match path {
Some(path) => toml::from_str(&fs::read_to_string(path)?)?,
None => Self::local_defaults(),
};
config.apply_environment()?;
config.validate()?;
Ok(config)
}
pub fn validate(&self) -> Result<(), ConfigError> {
self.server
.bind
.parse::<std::net::SocketAddr>()
.map_err(|_| ConfigError::Invalid("server.bind must be a socket address".into()))?;
if self.server.shutdown_grace_seconds == 0
|| self.server.shutdown_grace_seconds > 300
|| self.scheduler.max_concurrent_executions < 2
|| self.scheduler.max_concurrent_executions > 10_000
{
return Err(ConfigError::Invalid(
"shutdown grace must be 1..=300s and execution concurrency 2..=10000".into(),
));
}
if (!self.server.transport.agent_tasks
&& !self.server.transport.openai_responses
&& !self.server.transport.claude_messages)
|| !(1024..=16 * 1024 * 1024)
.contains(&self.server.transport.websocket_max_message_bytes)
{
return Err(ConfigError::Invalid(
"enable at least one public API/transport and bound WebSocket messages to 1KiB..=16MiB"
.into(),
));
}
let gateway = url::Url::parse(&self.agent_infra.gateway_url)
.map_err(|_| ConfigError::Invalid("Agent Infra Gateway URL is invalid".into()))?;
if gateway.host_str().is_none()
|| !gateway.username().is_empty()
|| gateway.password().is_some()
{
return Err(ConfigError::Invalid(
"Agent Infra Gateway URL requires a host and must not contain userinfo".into(),
));
}
if self.agent_infra.request_timeout_ms == 0
|| self.agent_infra.request_timeout_ms > 600_000
|| !(1024..=1024 * 1024 * 1024).contains(&self.agent_infra.max_response_bytes)
{
return Err(ConfigError::Invalid(
"Agent Infra request/body limits are invalid".into(),
));
}
if self.agent_infra.workload_subject.trim().is_empty()
|| self.agent_infra.workload_token.is_empty()
|| !(30..=7 * 24 * 60 * 60).contains(&self.agent_infra.delegation_ttl_seconds)
{
return Err(ConfigError::Invalid(
"Agent Infra workload identity/delegation settings are invalid".into(),
));
}
if self.binding.runtime_id.trim().is_empty()
|| !self.binding.agent_id.starts_with("agt_")
|| self.binding.agent_id.len() > 255
|| !self.binding.agent_id.bytes().all(|byte| {
byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':' | b'@')
})
|| self.binding.model.trim().is_empty()
|| self.binding.workspace_id.trim().is_empty()
{
return Err(ConfigError::Invalid(
"binding requires an Infra runtime ID, an agt_* Agent ID, model, and workspace ID"
.into(),
));
}
let JournalConfig::Memory {
max_events_per_execution,
} = &self.journal;
if !(1..=1_000_000).contains(max_events_per_execution) {
return Err(ConfigError::Invalid(
"memory journal events must be 1..=1000000".into(),
));
}
if let AuthConfig::LocalStatic { token, .. } = &self.auth
&& token.is_empty()
{
return Err(ConfigError::Invalid("local auth token is empty".into()));
}
if let AuthConfig::TrustedGateway { shared_secret } = &self.auth
&& !(32..=4096).contains(&shared_secret.expose().len())
{
return Err(ConfigError::Invalid(
"trusted gateway shared secret must contain 32..=4096 bytes".into(),
));
}
if self.mode == RuntimeMode::Production {
if !matches!(self.auth, AuthConfig::TrustedGateway { .. }) {
return Err(ConfigError::Invalid(
"production requires trusted gateway ingress authentication".into(),
));
}
if gateway.scheme() != "https" || self.agent_infra.trusted_mesh_http {
return Err(ConfigError::Invalid(
"production Agent Infra Gateway must use HTTPS".into(),
));
}
}
let compaction = &self.context.compaction;
if self.context.max_messages < 2
|| self.context.max_input_tokens < 1_024
|| u64::from(self.context.reserved_output_tokens) >= self.context.max_input_tokens
|| self.context.protocol_overhead_tokens >= self.context.max_input_tokens
|| !(1..=99).contains(&compaction.target_percent)
|| compaction.trigger_percent <= compaction.target_percent
|| compaction.emergency_percent <= compaction.trigger_percent
|| compaction.emergency_percent > 100
|| compaction.min_reclaim_percent > 100
|| !(1_024..=1_000_000).contains(&compaction.checkpoint_max_chars)
{
return Err(ConfigError::Invalid(
"context limits and compaction watermarks are invalid".into(),
));
}
Ok(())
}
fn apply_environment(&mut self) -> Result<(), ConfigError> {
if let Ok(value) = std::env::var("RUNTIME_BIND") {
self.server.bind = value;
}
apply_bool_env(
"RUNTIME_AGENT_TASKS_ENABLED",
&mut self.server.transport.agent_tasks,
)?;
apply_bool_env(
"RUNTIME_OPENAI_RESPONSES_ENABLED",
&mut self.server.transport.openai_responses,
)?;
apply_bool_env(
"RUNTIME_CLAUDE_MESSAGES_ENABLED",
&mut self.server.transport.claude_messages,
)?;
apply_bool_env("RUNTIME_SSE_ENABLED", &mut self.server.transport.sse)?;
apply_bool_env(
"RUNTIME_WEBSOCKET_ENABLED",
&mut self.server.transport.websocket,
)?;
if let Ok(value) = std::env::var("AGENT_INFRA_GATEWAY_URL") {
self.agent_infra.gateway_url = value;
}
if let Ok(value) = std::env::var("AGENT_INFRA_WORKLOAD_TOKEN") {
self.agent_infra.workload_token = SecretString(value);
}
if let Ok(value) = std::env::var("AGENT_INFRA_WORKLOAD_SUBJECT") {
self.agent_infra.workload_subject = value;
}
if let Ok(value) = std::env::var("AGENT_INFRA_RUNTIME_ID") {
self.binding.runtime_id = value;
}
if let Ok(value) = std::env::var("AGENT_INFRA_AGENT_ID") {
self.binding.agent_id = value;
}
if let Ok(value) = std::env::var("RUNTIME_MODEL") {
self.binding.model = value;
}
if let Ok(value) = std::env::var("RUNTIME_WORKSPACE_ID") {
self.binding.workspace_id = value;
}
if let Ok(value) = std::env::var("RUNTIME_LOCAL_TOKEN") {
match &mut self.auth {
AuthConfig::LocalStatic { token, .. } => *token = SecretString(value),
AuthConfig::TrustedGateway { .. } => {
return Err(ConfigError::Invalid(
"RUNTIME_LOCAL_TOKEN cannot override trusted gateway auth".into(),
));
}
}
}
if let Ok(value) = std::env::var("RUNTIME_GATEWAY_SHARED_SECRET") {
match &mut self.auth {
AuthConfig::TrustedGateway { shared_secret } => {
*shared_secret = SecretString(value)
}
AuthConfig::LocalStatic { .. } => {
return Err(ConfigError::Invalid(
"RUNTIME_GATEWAY_SHARED_SECRET requires trusted gateway auth".into(),
));
}
}
}
if self.mode == RuntimeMode::Local {
if self.agent_infra.workload_token.is_empty() {
self.agent_infra.workload_token =
SecretString("local-runtime-workload-token".into());
}
if let AuthConfig::LocalStatic { token, .. } = &mut self.auth
&& token.is_empty()
{
*token = SecretString("local-runtime-token".into());
}
}
Ok(())
}
fn local_defaults() -> Self {
Self {
mode: RuntimeMode::Local,
server: ServerConfig::default(),
scheduler: SchedulerConfig::default(),
agent_infra: AgentInfraConfig {
workload_token: SecretString("local-runtime-workload-token".into()),
..AgentInfraConfig::default()
},
journal: JournalConfig::default(),
auth: AuthConfig::default(),
binding: AgentBindingConfig {
runtime_id: "runtime-local".into(),
agent_id: "agt_local".into(),
model: "local-model".into(),
workspace_id: "workspace-local".into(),
definition_path: None,
},
context: ContextConfig::default(),
}
}
}
fn apply_bool_env(name: &str, target: &mut bool) -> Result<(), ConfigError> {
let Ok(value) = std::env::var(name) else {
return Ok(());
};
*target = value
.parse::<bool>()
.map_err(|_| ConfigError::Invalid(format!("{name} must be either true or false")))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_unknown_fields_and_redacts_secrets() {
let error = toml::from_str::<RuntimeConfig>("unknown = true").unwrap_err();
assert!(error.to_string().contains("unknown field"));
assert_eq!(format!("{:?}", SecretString("secret".into())), "[REDACTED]");
}
#[test]
fn production_accepts_memory_but_rejects_local_auth() {
let mut settings = RuntimeConfig::local_defaults();
settings.mode = RuntimeMode::Production;
assert!(settings.validate().is_err());
settings.agent_infra.gateway_url = "https://agent-infra.example.internal".into();
settings.agent_infra.trusted_mesh_http = false;
settings.auth = AuthConfig::TrustedGateway {
shared_secret: SecretString("a-secure-gateway-secret-with-32-bytes".into()),
};
assert!(settings.validate().is_ok());
}
#[test]
fn toml_uses_validated_defaults_for_omitted_context_policy() {
let toml: RuntimeConfig = toml::from_str(
r#"
[agent_infra]
gateway_url = "http://127.0.0.1:5100"
[binding]
runtime_id = "runtime-local"
agent_id = "agt_local"
model = "local-model"
workspace_id = "workspace-local"
[context]
max_input_tokens = 131072
"#,
)
.unwrap();
assert_eq!(toml.context.max_input_tokens, 128 * 1024);
assert_eq!(toml.server.bind, "127.0.0.1:8080");
assert_eq!(toml.scheduler.max_concurrent_executions, 16);
}
#[test]
fn binding_requires_infra_agent_id() {
let mut settings = RuntimeConfig::local_defaults();
settings.binding.agent_id = "runtime-code-agent".into();
assert!(settings.validate().is_err());
}
#[test]
fn at_least_one_execution_transport_must_be_explicitly_enabled() {
let mut settings = RuntimeConfig::local_defaults();
if let AuthConfig::LocalStatic { token, .. } = &mut settings.auth {
*token = SecretString("test-token".into());
}
settings.server.transport.agent_tasks = false;
settings.server.transport.openai_responses = false;
settings.server.transport.claude_messages = false;
settings.server.transport.sse = false;
settings.server.transport.websocket = false;
assert!(settings.validate().is_err());
settings.server.transport.agent_tasks = true;
settings.server.transport.websocket = true;
assert!(settings.validate().is_ok());
}
#[test]
fn checked_in_toml_examples_keep_only_required_binding_values() {
let local: RuntimeConfig =
toml::from_str(include_str!("../../../config/runtime.local.toml")).unwrap();
let production: RuntimeConfig = toml::from_str(include_str!(
"../../../config/runtime.production.example.toml"
))
.unwrap();
assert_eq!(local.binding.agent_id, "agt_local");
assert_eq!(local.server.transport, TransportConfig::default());
assert_eq!(local.server.bind, ServerConfig::default().bind);
assert_eq!(
local.server.shutdown_grace_seconds,
ServerConfig::default().shutdown_grace_seconds
);
assert_eq!(production.binding.runtime_id, "rti_generated_by_infra");
assert_eq!(production.context.max_messages, 128);
}
}