use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use khive_types::{namespace::Namespace, SubstrateKind};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{
config::{parse_embedding_model_alias, BackendId},
presentation::OutputFormat,
};
#[path = "engine_config_backend_disk_guard.rs"]
mod backend_disk_guard;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error(transparent)]
Credential(#[from] crate::credentials::CredentialError),
#[error("mount configuration: {reason}")]
InvalidMountConfig { reason: String },
#[error("config file I/O: {0}")]
Io(#[from] std::io::Error),
#[error("config TOML parse error in {path}: {source}")]
Parse {
path: PathBuf,
#[source]
source: toml::de::Error,
},
#[error("exactly one engine must be marked `default = true`; found {found}")]
DefaultCount { found: usize },
#[error("duplicate engine name: {name:?}")]
DuplicateName { name: String },
#[error(
"engine {name:?}: model {model:?} is not a recognized lattice_embed::EmbeddingModel name"
)]
UnknownModel { name: String, model: String },
#[error("engine {name:?}: fusion_weight must be > 0, got {value}")]
InvalidFusionWeight { name: String, value: f64 },
#[error(
"engine {name:?}: fusion_weight is not applied by current retrieval; \
remove it until weighted multi-engine fusion is wired"
)]
UnsupportedFusionWeight { name: String },
#[error("actor.id {id:?} is not a valid namespace: {reason}")]
InvalidActorId { id: String, reason: String },
#[error("[actor].mailbox_readers: {reason}")]
InvalidMailboxReaders { reason: String },
#[error("[gate].granted_actors entry {id:?} is not a valid actor id: {reason}")]
InvalidGrantedActorId { id: String, reason: String },
#[error("[gate].deny_writes_for is invalid: {reason}")]
InvalidWriteDenyPatterns { reason: String },
#[error("duplicate backend name: {name:?}")]
DuplicateBackendName { name: String },
#[error("invalid backend name {name:?}: {reason}")]
InvalidBackendName { name: String, reason: String },
#[error("backend {name:?}: `served_kinds` must not be empty when declared")]
EmptyBackendServedKinds { name: String },
#[error("backend {name:?}: invalid disk guard configuration: {reason}")]
InvalidBackendDiskGuard { name: String, reason: String },
#[error(
"backends {first_backend:?} and {second_backend:?} name the same database but resolve \
different disk reserve/deadline policies"
)]
DiskGuardAliasConflict {
first_backend: String,
second_backend: String,
},
#[error("KHIVE_SQLITE_WAL_CEILING_BYTES must be an unsigned decimal byte count")]
InvalidWalCeilingEnvironment { value: String },
#[error(
"backend {name:?}: wal_ceiling_bytes {value} exceeds supported SQLite offset arithmetic"
)]
WalCeilingOffsetOverflow { name: String, value: u64 },
#[error(
"backend {name:?}: nonzero wal_ceiling_bytes {value} requires a file-backed SQLite backend"
)]
WalCeilingMemoryBackend { name: String, value: u64 },
#[error("backend {name:?}: nonzero wal_ceiling_bytes {value} requires SQLite WAL mode")]
WalCeilingNonWalBackend { name: String, value: u64 },
#[error(
"backends {first_backend:?} and {second_backend:?} name the same database at {} \
but resolve different WAL ceilings ({first_bytes} and {second_bytes} bytes)",
crate::secret_gate::bounded_masked_log_text(&path.to_string_lossy())
)]
WalCeilingAliasConflict {
first_backend: String,
second_backend: String,
path: PathBuf,
first_bytes: u64,
second_bytes: u64,
},
#[error(
"backend configuration leaves searchable substrate kinds {kinds:?} unserved; \
defined backends: {defined}"
)]
MissingBackendSearchKinds {
kinds: Vec<SubstrateKind>,
defined: String,
},
#[error(
"[packs.{pack}].backend = {backend:?} references an unknown backend; \
defined backends: {defined}"
)]
UnknownPackBackend {
pack: String,
backend: String,
defined: String,
},
#[error(
"[[backends]] entry {name:?}: field `{field}` is not yet supported; \
remove it from the config or wait for a future release that implements it"
)]
UnsupportedBackendField { name: String, field: &'static str },
#[error(
"top-level `db = {value:?}` is not a supported config-file key; \
use `--db` / `KHIVE_DB` to select a single-file database, or \
`[[backends]].path` to declare storage backend topology"
)]
UnsupportedTopLevelDb { value: String },
#[error("[[git_write.allowed]] entry {repo:?}: {reason}")]
InvalidGitWriteEntry { repo: String, reason: String },
#[error("[git_write] {key}: {reason}")]
InvalidGitWriteConfig { key: String, reason: String },
#[error("[exec] {key}: {reason}")]
InvalidExecConfig { key: String, reason: String },
#[error("{entry}: {reason}")]
InvalidTelemetryConfig { entry: String, reason: String },
#[error("[web] {key}: {reason}")]
InvalidWebConfig { key: String, reason: String },
#[error(
"[runtime] blob_hydration_bytes must be between {min} and {max} bytes inclusive; got {value}"
)]
InvalidBlobHydrationBytes { value: u64, min: u64, max: u64 },
#[error("the explicitly selected config file does not exist: {path}")]
ExplicitConfigMissing { path: PathBuf },
#[error("[gate] configuration is not supported by this build")]
UnsupportedGateSection,
#[error(
"[display] timezone {timezone:?} is not a recognized IANA zone name (e.g. \"America/New_York\", \"UTC\")"
)]
InvalidDisplayTimezone { timezone: String },
#[error("{source} (config file: {})", path.display())]
InFile {
path: PathBuf,
#[source]
source: Box<ConfigError>,
},
}
impl ConfigError {
fn in_file(self, path: &Path) -> Self {
match self {
already @ (ConfigError::Parse { .. }
| ConfigError::ExplicitConfigMissing { .. }
| ConfigError::InFile { .. }) => already,
other => ConfigError::InFile {
path: path.to_path_buf(),
source: Box::new(other),
},
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct EngineConfig {
pub name: String,
pub model: String,
#[serde(default)]
pub default: bool,
pub fusion_weight: Option<f64>,
pub dims: Option<u32>,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct ActorConfig {
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub display_name: Option<String>,
#[serde(default)]
pub mailbox_readers: Vec<String>,
#[serde(default)]
pub visible_namespaces: Option<Vec<String>>,
#[serde(default)]
pub allowed_outbound_namespaces: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Default, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct GateSectionConfig {
#[serde(default)]
pub granted_actors: Vec<String>,
#[serde(default)]
pub grant_unattributed: bool,
#[serde(default)]
pub deny_writes_for: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum BackendKind {
#[default]
Sqlite,
Memory,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResolvedWalCeiling {
pub configured_bytes: u64,
pub effective_bytes: u64,
pub source: khive_db::WalCeilingSource,
}
pub fn resolve_wal_ceiling(
backend_field: Option<u64>,
env_value: Option<&str>,
backend_name: &str,
kind: BackendKind,
wal_mode: bool,
read_only: bool,
) -> Result<ResolvedWalCeiling, ConfigError> {
if kind == BackendKind::Memory && backend_field.is_none() {
return Ok(ResolvedWalCeiling {
configured_bytes: 0,
effective_bytes: 0,
source: khive_db::WalCeilingSource::Default,
});
}
let (configured_bytes, source) = if let Some(bytes) = backend_field {
(bytes, khive_db::WalCeilingSource::BackendField)
} else if let Some(raw) = env_value {
if raw.is_empty() || !raw.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(ConfigError::InvalidWalCeilingEnvironment {
value: raw.to_owned(),
});
}
let bytes = raw
.parse::<u64>()
.map_err(|_| ConfigError::InvalidWalCeilingEnvironment {
value: raw.to_owned(),
})?;
(bytes, khive_db::WalCeilingSource::Environment)
} else {
(0, khive_db::WalCeilingSource::Default)
};
if configured_bytes != 0 {
if i64::try_from(configured_bytes).is_err() {
return Err(ConfigError::WalCeilingOffsetOverflow {
name: backend_name.to_owned(),
value: configured_bytes,
});
}
if kind == BackendKind::Memory {
return Err(ConfigError::WalCeilingMemoryBackend {
name: backend_name.to_owned(),
value: configured_bytes,
});
}
if !wal_mode {
return Err(ConfigError::WalCeilingNonWalBackend {
name: backend_name.to_owned(),
value: configured_bytes,
});
}
}
Ok(ResolvedWalCeiling {
configured_bytes,
effective_bytes: if read_only { 0 } else { configured_bytes },
source,
})
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BackendConfig {
pub name: String,
#[serde(default)]
pub kind: BackendKind,
pub path: Option<std::path::PathBuf>,
pub cache_mb: Option<u32>,
pub journal_mode: Option<String>,
#[serde(default)]
pub served_kinds: Option<BTreeSet<SubstrateKind>>,
#[serde(default)]
pub read_only: bool,
pub wal_ceiling_bytes: Option<u64>,
#[serde(default)]
pub disk_reserve_bytes: Option<u64>,
#[serde(default)]
pub disk_guard_deadline_ms: Option<u64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PackConfig {
pub backend: String,
#[serde(default)]
pub no_embed: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "backend", rename_all = "lowercase", deny_unknown_fields)]
pub enum BlobConfig {
Fs {
#[serde(default)]
root: Option<String>,
#[serde(default)]
floor_bytes: Option<u64>,
},
S3 {
bucket: String,
region: String,
#[serde(default)]
endpoint: Option<String>,
#[serde(default)]
prefix: Option<String>,
#[serde(default)]
allow_http: Option<bool>,
},
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct BlobSectionConfig {
#[serde(default)]
pub file_transfers: bool,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct StorageSectionConfig {
#[serde(default)]
pub blob: Option<BlobConfig>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(deny_unknown_fields)]
pub struct BrainSectionConfig {
#[serde(default)]
pub fleet_readers: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GitWriteEntryConfig {
pub repo: String,
pub branches: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GitWriteSectionConfig {
#[serde(default)]
pub program: Option<PathBuf>,
#[serde(default)]
pub allowed: Vec<GitWriteEntryConfig>,
#[serde(default)]
pub actors: BTreeMap<String, GitWriteActorConfig>,
#[serde(default)]
pub repositories: BTreeMap<String, GitWriteRepositoryConfig>,
#[serde(default = "default_git_credential_resolver")]
pub credential_resolver: Vec<String>,
#[serde(default)]
pub contract_faults: bool,
#[serde(default)]
pub fault: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct GitWriteRepositoryConfig {
pub remote: String,
pub slug: String,
pub visibility: String,
#[serde(default)]
pub merge_refusals: Vec<String>,
}
impl GitWriteRepositoryConfig {
pub const MERGE_REFUSALS: [&'static str; 2] = ["opener", "last_pusher"];
pub fn refuses_merge_by(&self, entry: &str) -> bool {
self.merge_refusals.iter().any(|listed| listed == entry)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct GitWriteActorConfig {
pub name: String,
pub email: String,
pub credential_ref: String,
pub platform_identity: String,
}
fn default_git_credential_resolver() -> Vec<String> {
[
"/usr/bin/security",
"find-generic-password",
"-w",
"-s",
"{ref}",
]
.into_iter()
.map(str::to_string)
.collect()
}
impl Default for GitWriteSectionConfig {
fn default() -> Self {
Self {
program: None,
allowed: Vec::new(),
actors: BTreeMap::new(),
repositories: BTreeMap::new(),
credential_resolver: default_git_credential_resolver(),
contract_faults: false,
fault: None,
}
}
}
impl GitWriteSectionConfig {
pub fn git_program(&self) -> &Path {
self.program.as_deref().unwrap_or_else(|| Path::new("git"))
}
pub fn validate_dev_loop(&self) -> Result<(), ConfigError> {
let invalid = |key: &str, reason: &str| ConfigError::InvalidGitWriteConfig {
key: key.to_string(),
reason: reason.to_string(),
};
if let Some(program) = &self.program {
if !program.is_absolute() {
return Err(invalid("git_write.program", "must be absolute"));
}
let metadata = std::fs::metadata(program).map_err(|error| {
if error.kind() == std::io::ErrorKind::NotFound {
invalid("git_write.program", "does not exist")
} else {
invalid("git_write.program", &format!("is not executable: {error}"))
}
})?;
#[cfg(unix)]
let executable = {
use std::os::unix::fs::PermissionsExt;
metadata.permissions().mode() & 0o111 != 0
};
#[cfg(windows)]
let executable = program
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| {
extension.eq_ignore_ascii_case("exe") || extension.eq_ignore_ascii_case("com")
});
#[cfg(not(any(unix, windows)))]
let executable = false;
if !metadata.is_file() || !executable {
return Err(invalid("git_write.program", "is not executable"));
}
}
if self.contract_faults && !cfg!(feature = "contract-faults") {
tracing::error!(
target: "khive.boot",
"[git_write] contract_faults requires the test-only contract-faults build feature"
);
return Err(invalid(
"contract_faults",
"requires the test-only contract-faults build feature",
));
}
if let Some(fault) = &self.fault {
if !self.contract_faults {
return Err(invalid("fault", "requires contract_faults = true"));
}
let valid = fault.split_once(':').is_some_and(|(verb, point)| {
matches!(verb, "git.push" | "git.pr_merge")
&& matches!(
point,
"reply-lost-after-effect" | "audit-fails-after-effect"
)
});
if !valid {
return Err(invalid("fault", "unsupported contract fault selector"));
}
}
for (path, repository) in &self.repositories {
let key = format!("repositories.{path}.merge_refusals");
let mut seen: Vec<&str> = Vec::new();
for entry in &repository.merge_refusals {
if !GitWriteRepositoryConfig::MERGE_REFUSALS.contains(&entry.as_str()) {
return Err(invalid(&key, "entries must be opener or last_pusher"));
}
if seen.contains(&entry.as_str()) {
return Err(invalid(&key, "entries must not repeat"));
}
seen.push(entry);
}
}
if !cfg!(unix)
&& self.actors.is_empty()
&& self.credential_resolver == default_git_credential_resolver()
{
return Ok(());
}
let argv = &self.credential_resolver;
let Some(program) = argv.first() else {
return Err(invalid("credential_resolver", "argv must not be empty"));
};
let program_path = Path::new(program);
if !program_path.is_absolute() {
return Err(invalid(
"credential_resolver",
"argv[0] must be an absolute path",
));
}
let program_name = program_path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or_default()
.to_ascii_lowercase();
if matches!(
program_name.trim_end_matches(".exe"),
"sh" | "bash"
| "dash"
| "zsh"
| "ksh"
| "fish"
| "csh"
| "tcsh"
| "cmd"
| "powershell"
| "pwsh"
| "env"
) {
return Err(invalid(
"credential_resolver",
"shell or env launcher is not allowed",
));
}
if argv.iter().any(|arg| arg.chars().any(char::is_control)) {
return Err(invalid(
"credential_resolver",
"argv must not contain control characters",
));
}
if program.contains(['{', '}'])
|| argv[1..]
.iter()
.any(|arg| arg != "{ref}" && arg.contains(['{', '}']))
{
return Err(invalid(
"credential_resolver",
"{ref} must be a complete argument and is the only allowed template",
));
}
if !argv[1..].iter().any(|arg| arg == "{ref}") {
return Err(invalid(
"credential_resolver",
"argv must contain a {ref} argument",
));
}
for (actor, identity) in &self.actors {
if actor.trim().is_empty() || actor.chars().any(char::is_control) {
return Err(invalid(
"actors",
"actor labels must be nonempty and contain no control characters",
));
}
for (field, value) in [
("name", &identity.name),
("email", &identity.email),
("credential_ref", &identity.credential_ref),
("platform_identity", &identity.platform_identity),
] {
if value.trim().is_empty() || value.chars().any(char::is_control) {
return Err(invalid(
&format!("actors.{actor}.{field}"),
"must be nonempty and contain no control characters",
));
}
}
if identity.name.contains(['<', '>']) || identity.email.contains(['<', '>']) {
return Err(invalid(
&format!("actors.{actor}"),
"name and email must not contain Git identity delimiters",
));
}
}
Ok(())
}
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ExecLimitsConfig {
#[serde(default)]
pub cpu_seconds: Option<u64>,
#[serde(default)]
pub address_space: Option<u64>,
#[serde(default)]
pub file_size: Option<u64>,
#[serde(default)]
pub nproc: Option<u64>,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct ExecSectionConfig {
#[serde(default)]
pub root: Option<String>,
#[serde(default)]
pub read_roots: Vec<String>,
#[serde(default)]
pub env: Vec<String>,
#[serde(default)]
pub never: Vec<String>,
#[serde(default)]
pub max_output_bytes: Option<u64>,
#[serde(default)]
pub timeout_default_s: Option<f64>,
#[serde(default)]
pub timeout_max_s: Option<f64>,
#[serde(default)]
pub binary_digest_timeout_s: Option<u64>,
#[serde(default)]
pub keep: bool,
#[serde(default)]
pub limits: ExecLimitsConfig,
}
pub const DEFAULT_EXEC_BINARY_DIGEST_TIMEOUT_S: u64 = 10;
pub const MAX_EXEC_BINARY_DIGEST_TIMEOUT_S: u64 = 60;
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct WebAllowlistEntry {
pub host: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct WebCredentialConfig {
pub name: String,
pub env_var: String,
pub hosts: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct WebFixtureResult {
pub title: String,
pub url: String,
pub snippet: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "kind", rename_all = "lowercase", deny_unknown_fields)]
pub enum WebSearchProviderConfig {
Fixture {
name: String,
#[serde(default)]
default: bool,
results: Vec<WebFixtureResult>,
},
Http {
name: String,
#[serde(default)]
default: bool,
url_template: String,
#[serde(default)]
api_key_env: Option<String>,
#[serde(default)]
hosts: Vec<String>,
},
}
impl WebSearchProviderConfig {
pub fn name(&self) -> &str {
match self {
WebSearchProviderConfig::Fixture { name, .. } => name,
WebSearchProviderConfig::Http { name, .. } => name,
}
}
pub fn is_default(&self) -> bool {
match self {
WebSearchProviderConfig::Fixture { default, .. } => *default,
WebSearchProviderConfig::Http { default, .. } => *default,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(deny_unknown_fields)]
pub struct WebSectionConfig {
#[serde(default)]
pub timeout_default_s: Option<u64>,
#[serde(default)]
pub timeout_max_s: Option<u64>,
#[serde(default)]
pub max_bytes_default: Option<u64>,
#[serde(default)]
pub max_bytes_max: Option<u64>,
#[serde(default)]
pub search_limit_default: Option<u32>,
#[serde(default)]
pub search_limit_max: Option<u32>,
#[serde(default)]
pub allowlist: Vec<WebAllowlistEntry>,
#[serde(default)]
pub credentials: Vec<WebCredentialConfig>,
#[serde(default)]
pub search_providers: Vec<WebSearchProviderConfig>,
#[serde(default)]
pub read_roots: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WebCeilings {
pub timeout_default_s: u64,
pub timeout_max_s: u64,
pub max_bytes_default: u64,
pub max_bytes_max: u64,
pub search_limit_default: u32,
pub search_limit_max: u32,
}
impl Default for WebCeilings {
fn default() -> Self {
Self {
timeout_default_s: 30,
timeout_max_s: 120,
max_bytes_default: 5 * 1024 * 1024,
max_bytes_max: 50 * 1024 * 1024,
search_limit_default: 10,
search_limit_max: 50,
}
}
}
impl WebSectionConfig {
pub fn resolved_ceilings(&self) -> Result<WebCeilings, ConfigError> {
let defaults = WebCeilings::default();
let bounds = WebCeilings {
timeout_default_s: self.timeout_default_s.unwrap_or(defaults.timeout_default_s),
timeout_max_s: self.timeout_max_s.unwrap_or(defaults.timeout_max_s),
max_bytes_default: self.max_bytes_default.unwrap_or(defaults.max_bytes_default),
max_bytes_max: self.max_bytes_max.unwrap_or(defaults.max_bytes_max),
search_limit_default: self
.search_limit_default
.unwrap_or(defaults.search_limit_default),
search_limit_max: self.search_limit_max.unwrap_or(defaults.search_limit_max),
};
for (key, default, maximum, maximum_key) in [
(
"timeout_default_s",
bounds.timeout_default_s,
bounds.timeout_max_s,
"timeout_max_s",
),
(
"max_bytes_default",
bounds.max_bytes_default,
bounds.max_bytes_max,
"max_bytes_max",
),
(
"search_limit_default",
u64::from(bounds.search_limit_default),
u64::from(bounds.search_limit_max),
"search_limit_max",
),
] {
if default == 0 || default > maximum {
return Err(ConfigError::InvalidWebConfig {
key: key.into(),
reason: format!(
"resolved default must be positive and not exceed {maximum_key}={maximum}"
),
});
}
}
if std::time::Instant::now()
.checked_add(std::time::Duration::from_secs(bounds.timeout_max_s))
.is_none()
{
return Err(ConfigError::InvalidWebConfig {
key: "timeout_max_s".into(),
reason: "cannot be represented as a request deadline".into(),
});
}
Ok(bounds)
}
pub fn validate(&self) -> Result<(), ConfigError> {
self.resolved_ceilings()?;
let invalid = |key: &str, reason: &str| ConfigError::InvalidWebConfig {
key: key.to_string(),
reason: reason.to_string(),
};
let mut seen_hosts = std::collections::HashSet::new();
for entry in &self.allowlist {
let normalized = entry.host.trim().trim_end_matches('.').to_ascii_lowercase();
if normalized.is_empty() {
return Err(invalid("allowlist.host", "must not be empty"));
}
if !seen_hosts.insert(normalized) {
return Err(invalid("allowlist.host", "duplicate host entry"));
}
}
let mut seen_credentials = std::collections::HashSet::new();
for credential in &self.credentials {
if credential.name.trim().is_empty() {
return Err(invalid("credentials.name", "must not be empty"));
}
if !seen_credentials.insert(credential.name.clone()) {
return Err(invalid("credentials.name", "duplicate credential name"));
}
if credential.env_var.trim().is_empty() {
return Err(invalid("credentials.env_var", "must not be empty"));
}
if credential.hosts.is_empty() {
return Err(invalid(
"credentials.hosts",
"must name at least one host or suffix",
));
}
}
let mut seen_providers = std::collections::HashSet::new();
let mut default_count = 0;
for provider in &self.search_providers {
let name = provider.name();
if name.trim().is_empty() {
return Err(invalid("search_providers.name", "must not be empty"));
}
if !seen_providers.insert(name.to_string()) {
return Err(invalid("search_providers.name", "duplicate provider name"));
}
if provider.is_default() {
default_count += 1;
}
if let WebSearchProviderConfig::Http {
url_template,
api_key_env,
hosts,
..
} = provider
{
if !url_template.contains("{query}") {
return Err(invalid(
"search_providers.url_template",
"must contain the literal substring {query}",
));
}
if api_key_env.is_some() && hosts.is_empty() {
return Err(invalid(
"search_providers.hosts",
"an api_key_env-bearing provider must name at least one host or suffix",
));
}
}
}
if default_count > 1 {
return Err(invalid(
"search_providers",
"at most one provider may set default = true",
));
}
Ok(())
}
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct KhiveConfig {
#[serde(skip)]
pub credentials: Vec<crate::credentials::CredentialConfig>,
#[serde(skip)]
pub visibility_receipts: Option<crate::credentials::VisibilityReceiptConfig>,
#[serde(default)]
pub mounts: Vec<crate::mount_config::MountConfig>,
#[serde(default)]
pub db: Option<String>,
#[serde(default)]
pub engines: Vec<EngineConfig>,
#[serde(default)]
pub actor: ActorConfig,
#[serde(default)]
pub gate: Option<GateSectionConfig>,
#[serde(default)]
pub runtime: RuntimeSectionConfig,
#[serde(default)]
pub backends: Vec<BackendConfig>,
#[serde(default)]
pub packs: std::collections::HashMap<String, PackConfig>,
#[serde(default)]
pub brain: BrainSectionConfig,
#[serde(default)]
pub git_write: GitWriteSectionConfig,
#[serde(default)]
pub blob: BlobSectionConfig,
#[serde(default)]
pub storage: StorageSectionConfig,
#[serde(default)]
pub exec: ExecSectionConfig,
#[serde(default)]
pub telemetry: crate::telemetry_config::TelemetryConfig,
#[serde(default)]
pub display: DisplaySectionConfig,
#[serde(default)]
pub web: WebSectionConfig,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct RuntimeSectionConfig {
#[serde(default)]
pub packs: Option<Vec<String>>,
#[serde(default)]
pub brain_profile: Option<String>,
#[serde(default)]
pub default_output_format: Option<OutputFormat>,
#[serde(default)]
pub blob_hydration_bytes: Option<u64>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct DisplaySectionConfig {
#[serde(default)]
pub timezone: Option<String>,
}
impl KhiveConfig {
pub fn load(path: Option<&Path>) -> Result<Option<Self>, ConfigError> {
let resolved = match path {
Some(p) => p.to_path_buf(),
None => PathBuf::from(".khive/config.toml"),
};
if !resolved.exists() {
return Ok(None);
}
let diagnostic_path = std::fs::canonicalize(&resolved).unwrap_or_else(|_| resolved.clone());
let raw = std::fs::read_to_string(&resolved)
.map_err(|source| ConfigError::from(source).in_file(&diagnostic_path))?;
let mut cfg: KhiveConfig = toml::from_str(&raw).map_err(|source| ConfigError::Parse {
path: diagnostic_path.clone(),
source,
})?;
crate::credentials::read_tables(&raw, &mut cfg)
.map_err(|error| ConfigError::from(error).in_file(&diagnostic_path))?;
cfg.validate()
.map_err(|error| error.in_file(&diagnostic_path))?;
Ok(Some(cfg))
}
pub fn load_with_home_fallback(
path: Option<&Path>,
db_path: Option<&Path>,
) -> Result<Option<Self>, ConfigError> {
Ok(Self::load_with_home_fallback_and_source(path, db_path)?.map(|(config, _)| config))
}
pub fn load_with_home_fallback_and_source(
path: Option<&Path>,
db_path: Option<&Path>,
) -> Result<Option<(Self, PathBuf)>, ConfigError> {
if let Some(p) = path {
if !p.exists() {
return Err(ConfigError::ExplicitConfigMissing {
path: p.to_path_buf(),
});
}
return Ok(Self::load(Some(p))?.map(|config| (config, Self::diagnostic_config_path(p))));
}
let project_root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let home_root = std::env::var_os("HOME").map(PathBuf::from);
Self::load_with_roots_and_source(&project_root, home_root.as_deref(), db_path)
}
#[cfg(test)]
pub(crate) fn load_with_roots(
project_root: &Path,
home_root: Option<&Path>,
db_path: Option<&Path>,
) -> Result<Option<Self>, ConfigError> {
Ok(
Self::load_with_roots_and_source(project_root, home_root, db_path)?
.map(|(config, _)| config),
)
}
fn load_with_roots_and_source(
project_root: &Path,
home_root: Option<&Path>,
db_path: Option<&Path>,
) -> Result<Option<(Self, PathBuf)>, ConfigError> {
let tier2 = project_root.join("khive.toml");
if tier2.exists() {
return Ok(Self::load(Some(&tier2))?
.map(|config| (config, Self::diagnostic_config_path(&tier2))));
}
let tier3 = Self::project_config_anchor_dir(db_path, project_root).join("config.toml");
if tier3.exists() {
return Ok(Self::load(Some(&tier3))?
.map(|config| (config, Self::diagnostic_config_path(&tier3))));
}
if let Some(home) = home_root {
let tier4 = home.join(".khive/config.toml");
if tier4.exists() {
return Ok(Self::load(Some(&tier4))?
.map(|config| (config, Self::diagnostic_config_path(&tier4))));
}
}
Ok(None)
}
fn diagnostic_config_path(path: &Path) -> PathBuf {
std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}
fn project_config_anchor_dir(db_path: Option<&Path>, project_root: &Path) -> PathBuf {
let Some(db_path) = db_path else {
return project_root.join(".khive");
};
let absolute = std::fs::canonicalize(db_path).unwrap_or_else(|_| {
if db_path.is_absolute() {
db_path.to_path_buf()
} else {
project_root.join(db_path)
}
});
let db_dir = absolute.parent().map(Path::to_path_buf).unwrap_or(absolute);
if db_dir.file_name().is_some_and(|name| name == ".khive") {
db_dir
} else {
db_dir.join(".khive")
}
}
pub fn validate(&self) -> Result<(), ConfigError> {
crate::mount_config::validate_mounts(&self.mounts)?;
self.git_write.validate_dev_loop()?;
self.telemetry.validate()?;
self.web.validate()?;
crate::credentials::CredentialConfig::validate_all(&self.credentials)?;
if let Some(receipts) = &self.visibility_receipts {
receipts.validate(&self.credentials)?;
}
if let Some(value) = self.db.as_deref() {
if !value.is_empty() {
return Err(ConfigError::UnsupportedTopLevelDb {
value: value.to_string(),
});
}
}
if cfg!(target_os = "macos") {
if self.exec.limits.address_space.is_some() {
return Err(ConfigError::InvalidExecConfig {
key: "limits.address_space".to_string(),
reason: "unsupported_on_platform: macOS does not enforce an address-space rlimit per process".to_string(),
});
}
if self.exec.limits.nproc.is_some() {
return Err(ConfigError::InvalidExecConfig {
key: "limits.nproc".to_string(),
reason: "unsupported_on_platform: RLIMIT_NPROC counts every process of the uid, not one run".to_string(),
});
}
}
let default_timeout = self.exec.timeout_default_s.unwrap_or(30.0);
let maximum_timeout = self.exec.timeout_max_s.unwrap_or(600.0);
for (key, value) in [
("timeout_default_s", default_timeout),
("timeout_max_s", maximum_timeout),
] {
let duration = value
.is_finite()
.then(|| std::time::Duration::try_from_secs_f64(value).ok())
.flatten()
.filter(|duration| !duration.is_zero());
if duration
.is_none_or(|duration| std::time::Instant::now().checked_add(duration).is_none())
{
return Err(ConfigError::InvalidExecConfig {
key: key.to_string(),
reason: "must be positive, finite, and representable as a deadline".to_string(),
});
}
}
if default_timeout > maximum_timeout {
return Err(ConfigError::InvalidExecConfig {
key: "timeout_default_s".to_string(),
reason: format!(
"default {default_timeout} exceeds timeout_max_s {maximum_timeout}"
),
});
}
let binary_digest_timeout = self
.exec
.binary_digest_timeout_s
.unwrap_or(DEFAULT_EXEC_BINARY_DIGEST_TIMEOUT_S);
if !(1..=MAX_EXEC_BINARY_DIGEST_TIMEOUT_S).contains(&binary_digest_timeout) {
return Err(ConfigError::InvalidExecConfig {
key: "binary_digest_timeout_s".to_string(),
reason: format!("must be between 1 and {MAX_EXEC_BINARY_DIGEST_TIMEOUT_S} seconds"),
});
}
if let Some(value) = self.runtime.blob_hydration_bytes {
let min = khive_storage::MAX_BLOB_WHOLE_BYTES;
let max = tokio::sync::Semaphore::MAX_PERMITS as u64;
if value < min || value > max {
return Err(ConfigError::InvalidBlobHydrationBytes { value, min, max });
}
}
if let Some(id) = self.actor.id.as_deref() {
if id.is_empty() {
return Err(ConfigError::InvalidActorId {
id: id.to_string(),
reason: "actor.id must not be empty; remove the key or provide a value"
.to_string(),
});
}
Namespace::parse(id).map_err(|e| ConfigError::InvalidActorId {
id: id.to_string(),
reason: e.to_string(),
})?;
}
self.actor
.mailbox_gate(std::sync::Arc::new(khive_gate::AllowAllGate))
.map_err(|error| ConfigError::InvalidMailboxReaders {
reason: error.to_string(),
})?;
if let Some(ref vis) = self.actor.visible_namespaces {
for ns_str in vis {
if ns_str.is_empty() {
return Err(ConfigError::InvalidActorId {
id: ns_str.clone(),
reason: "visible_namespaces entries must not be empty".to_string(),
});
}
Namespace::parse(ns_str).map_err(|e| ConfigError::InvalidActorId {
id: ns_str.clone(),
reason: format!("invalid visible namespace: {e}"),
})?;
}
}
if let Some(gate) = &self.gate {
khive_gate::CallerEnrollmentGate::validate_write_denials(&gate.deny_writes_for)
.map_err(|error| ConfigError::InvalidWriteDenyPatterns {
reason: error.to_string(),
})?;
for id in &gate.granted_actors {
if id.is_empty() {
return Err(ConfigError::InvalidGrantedActorId {
id: id.clone(),
reason: "actor ids must not be empty".to_string(),
});
}
Namespace::parse(id).map_err(|error| ConfigError::InvalidGrantedActorId {
id: id.clone(),
reason: error.to_string(),
})?;
}
}
for ns_str in &self.actor.allowed_outbound_namespaces {
if ns_str.is_empty() {
return Err(ConfigError::InvalidActorId {
id: ns_str.clone(),
reason: "allowed_outbound_namespaces entries must not be empty".to_string(),
});
}
Namespace::parse(ns_str).map_err(|e| ConfigError::InvalidActorId {
id: ns_str.clone(),
reason: format!("invalid allowed_outbound_namespaces entry: {e}"),
})?;
}
if !self.backends.is_empty() {
let mut seen_backends = std::collections::HashSet::new();
for backend in &self.backends {
BackendId::parse(&backend.name).map_err(|error| {
ConfigError::InvalidBackendName {
name: backend.name.clone(),
reason: error.to_string(),
}
})?;
if backend
.served_kinds
.as_ref()
.is_some_and(BTreeSet::is_empty)
{
return Err(ConfigError::EmptyBackendServedKinds {
name: backend.name.clone(),
});
}
if !seen_backends.insert(backend.name.clone()) {
return Err(ConfigError::DuplicateBackendName {
name: backend.name.clone(),
});
}
if backend.wal_ceiling_bytes.is_some() {
resolve_wal_ceiling(
backend.wal_ceiling_bytes,
None,
&backend.name,
backend.kind.clone(),
backend
.journal_mode
.as_deref()
.is_none_or(|mode| mode.eq_ignore_ascii_case("wal")),
backend.read_only,
)?;
}
backend.resolve_disk_guard(&khive_db::DiskGuardEnvironment::default())?;
if backend.cache_mb.is_some() {
return Err(ConfigError::UnsupportedBackendField {
name: backend.name.clone(),
field: "cache_mb",
});
}
if backend.journal_mode.is_some() {
return Err(ConfigError::UnsupportedBackendField {
name: backend.name.clone(),
field: "journal_mode",
});
}
}
}
let defined: Vec<&str> = if self.backends.is_empty() {
vec![BackendId::MAIN]
} else {
self.backends.iter().map(|b| b.name.as_str()).collect()
};
for (pack_name, pack_cfg) in &self.packs {
if !defined.contains(&pack_cfg.backend.as_str()) {
return Err(ConfigError::UnknownPackBackend {
pack: pack_name.clone(),
backend: pack_cfg.backend.clone(),
defined: defined.join(", "),
});
}
}
if !self.backends.is_empty() {
let missing: Vec<_> = [SubstrateKind::Note, SubstrateKind::Entity]
.into_iter()
.filter(|kind| {
!self.backends.iter().any(|backend| {
backend
.served_kinds
.as_ref()
.is_none_or(|served| served.contains(kind))
})
})
.collect();
if !missing.is_empty() {
return Err(ConfigError::MissingBackendSearchKinds {
kinds: missing,
defined: defined.join(", "),
});
}
}
if let Some(tz) = self.display.timezone.as_deref() {
if tz.trim().is_empty() || tz.parse::<chrono_tz::Tz>().is_err() {
return Err(ConfigError::InvalidDisplayTimezone {
timezone: tz.to_string(),
});
}
}
for entry in &self.git_write.allowed {
if entry.repo.trim().is_empty() {
return Err(ConfigError::InvalidGitWriteEntry {
repo: entry.repo.clone(),
reason: "repo must not be empty".to_string(),
});
}
if !Path::new(&entry.repo).is_absolute() {
return Err(ConfigError::InvalidGitWriteEntry {
repo: entry.repo.clone(),
reason: "repo must be an absolute path".to_string(),
});
}
if entry.branches.is_empty() {
return Err(ConfigError::InvalidGitWriteEntry {
repo: entry.repo.clone(),
reason: "branches must not be empty".to_string(),
});
}
if entry.branches.iter().any(|b| b.trim().is_empty()) {
return Err(ConfigError::InvalidGitWriteEntry {
repo: entry.repo.clone(),
reason: "branches entries must not be empty".to_string(),
});
}
if let Some(bad) = entry.branches.iter().find(|b| b.matches('*').count() > 1) {
return Err(ConfigError::InvalidGitWriteEntry {
repo: entry.repo.clone(),
reason: format!(
"branch pattern {bad:?} must contain at most one '*' wildcard (ADR-108)"
),
});
}
}
if self.engines.is_empty() {
return Ok(());
}
let mut seen_names = std::collections::HashSet::new();
for engine in &self.engines {
if !seen_names.insert(engine.name.clone()) {
return Err(ConfigError::DuplicateName {
name: engine.name.clone(),
});
}
if parse_embedding_model_alias(&engine.model).is_none() {
return Err(ConfigError::UnknownModel {
name: engine.name.clone(),
model: engine.model.clone(),
});
}
}
let default_count = self.engines.iter().filter(|e| e.default).count();
if default_count != 1 {
return Err(ConfigError::DefaultCount {
found: default_count,
});
}
for engine in &self.engines {
if let Some(w) = engine.fusion_weight {
if !w.is_finite() || w <= 0.0 {
return Err(ConfigError::InvalidFusionWeight {
name: engine.name.clone(),
value: w,
});
}
}
}
if let Some(engine) = self
.engines
.iter()
.find(|engine| engine.fusion_weight.is_some())
{
return Err(ConfigError::UnsupportedFusionWeight {
name: engine.name.clone(),
});
}
Ok(())
}
pub fn default_engine(&self) -> Option<&EngineConfig> {
self.engines.iter().find(|e| e.default)
}
}
pub fn config_from_env() -> KhiveConfig {
let primary_model = std::env::var("KHIVE_EMBEDDING_MODEL")
.ok()
.filter(|s| !s.trim().is_empty());
let additional_raw = std::env::var("KHIVE_ADDITIONAL_EMBEDDING_MODELS")
.ok()
.unwrap_or_default();
let additional: Vec<String> = crate::runtime::parse_pack_list(&additional_raw)
.into_iter()
.filter(|s| !s.is_empty())
.collect();
if primary_model.is_none() && additional.is_empty() {
return KhiveConfig::default();
}
tracing::info!(
"using env-var embedding config; consider migrating to .khive/config.toml in your project root"
);
config_from_env_parts(primary_model, additional)
}
fn config_from_env_parts(primary_model: Option<String>, additional: Vec<String>) -> KhiveConfig {
let mut engines = Vec::new();
let primary =
primary_model.unwrap_or_else(|| lattice_embed::EmbeddingModel::AllMiniLmL6V2.to_string());
engines.push(EngineConfig {
name: "default".to_string(),
model: primary.clone(),
default: true,
fusion_weight: None,
dims: None,
});
for (i, model) in additional.into_iter().enumerate() {
if model.eq_ignore_ascii_case(&primary) {
continue;
}
engines.push(EngineConfig {
name: format!("engine-{}", i + 1),
model,
default: false,
fusion_weight: None,
dims: None,
});
}
KhiveConfig {
engines,
..KhiveConfig::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
include!("engine_config_timeout_tests.rs");
include!("engine_config_deadline_tests.rs");
include!("engine_config_disk_guard_tests.rs");
fn write_toml(dir: &tempfile::TempDir, content: &str) -> PathBuf {
let path = dir.path().join("config.toml");
std::fs::write(&path, content).unwrap();
path
}
fn in_memory_runtime_config() -> crate::RuntimeConfig {
crate::RuntimeConfig {
db_path: None,
..crate::RuntimeConfig::no_embeddings()
}
}
#[test]
fn load_errors_name_the_config_file() {
let dir = tempfile::tempdir().unwrap();
let gate = write_toml(&dir, "[gate]\nmode = \"x\"\n");
let err = KhiveConfig::load(Some(&gate)).expect_err("unknown gate key must fail");
assert!(
err.to_string().contains(&gate.display().to_string()),
"gate error must name the file, got: {err}"
);
let invalid = write_toml(
&dir,
"[[engines]]\nname = \"a\"\nmodel = \"all-minilm-l6-v2\"\n",
);
let err = KhiveConfig::load(Some(&invalid)).expect_err("validation must fail");
assert!(
err.to_string().contains("(config file: "),
"validation error must name the file, got: {err}"
);
let parse = write_toml(&dir, "not = = toml");
let err = KhiveConfig::load(Some(&parse)).expect_err("parse must fail");
assert!(
err.to_string().contains("config.toml"),
"parse error must name the file, got: {err}"
);
assert!(
matches!(err, ConfigError::Parse { .. }),
"parse errors keep their own variant unwrapped, got: {err:?}"
);
}
fn config_error_root(err: &ConfigError) -> &ConfigError {
match err {
ConfigError::InFile { path, source } => {
assert!(
!path.as_os_str().is_empty(),
"InFile must carry the config path"
);
source
}
other => other,
}
}
include!("engine_config_env_additional_tests.rs");
#[test]
fn test_load_minimal_config() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[engines]]
name = "x"
model = "all-minilm-l6-v2"
default = true
"#,
);
let cfg = KhiveConfig::load(Some(&path))
.expect("load should succeed")
.expect("file should be found");
assert_eq!(cfg.engines.len(), 1);
assert_eq!(cfg.engines[0].name, "x");
assert_eq!(cfg.engines[0].model, "all-minilm-l6-v2");
assert!(cfg.engines[0].default);
}
#[test]
fn test_unknown_engine_model_rejected_before_conversion() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
"[[engines]]\nname = \"primary\"\nmodel = \"not-a-model\"\ndefault = true\n",
);
let err = KhiveConfig::load(Some(&path)).expect_err("unknown primary model must fail");
assert!(
matches!(
config_error_root(&err),
ConfigError::UnknownModel { name, model }
if name == "primary" && model == "not-a-model"
),
"expected UnknownModel for the primary engine, got {err:?}"
);
let config: KhiveConfig = toml::from_str(
"[[engines]]\nname = \"primary\"\nmodel = \"all-minilm-l6-v2\"\ndefault = true\n\n[[engines]]\nname = \"secondary\"\nmodel = \"not-a-model\"\n",
)
.unwrap();
assert!(matches!(
config.validate(),
Err(ConfigError::UnknownModel { name, model })
if name == "secondary" && model == "not-a-model"
));
}
#[test]
fn test_recognized_engine_model_validates_and_converts() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
"[[engines]]\nname = \"primary\"\nmodel = \"all-minilm-l6-v2\"\ndefault = true\n",
);
let config = KhiveConfig::load(Some(&path)).unwrap().unwrap();
config.validate().unwrap();
let runtime = crate::runtime_config_from_khive_config(&config, in_memory_runtime_config());
assert_eq!(
runtime.embedding_model,
Some(lattice_embed::EmbeddingModel::AllMiniLmL6V2)
);
}
#[test]
fn test_default_engine_required_when_engines_present() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[engines]]
name = "a"
model = "all-minilm-l6-v2"
"#,
);
let err = KhiveConfig::load(Some(&path)).expect_err("should fail with no default flagged");
assert!(
matches!(
config_error_root(&err),
ConfigError::DefaultCount { found: 0 }
),
"expected DefaultCount {{ found: 0 }}, got {err:?}"
);
}
#[test]
fn test_multiple_default_rejected() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[engines]]
name = "a"
model = "all-minilm-l6-v2"
default = true
[[engines]]
name = "b"
model = "paraphrase-multilingual-minilm-l12-v2"
default = true
"#,
);
let err = KhiveConfig::load(Some(&path)).expect_err("should fail with two defaults");
assert!(
matches!(
config_error_root(&err),
ConfigError::DefaultCount { found: 2 }
),
"expected DefaultCount {{ found: 2 }}, got {err:?}"
);
}
#[test]
fn test_fusion_weight_validation() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[engines]]
name = "a"
model = "all-minilm-l6-v2"
default = true
fusion_weight = -0.5
"#,
);
let err =
KhiveConfig::load(Some(&path)).expect_err("should fail with negative fusion_weight");
assert!(
matches!(
config_error_root(&err),
ConfigError::InvalidFusionWeight { .. }
),
"expected InvalidFusionWeight, got {err:?}"
);
let path2 = write_toml(
&dir,
r#"
[[engines]]
name = "a"
model = "all-minilm-l6-v2"
default = true
fusion_weight = 0.0
"#,
);
let err2 =
KhiveConfig::load(Some(&path2)).expect_err("should fail with zero fusion_weight");
assert!(
matches!(
config_error_root(&err2),
ConfigError::InvalidFusionWeight { .. }
),
"expected InvalidFusionWeight, got {err2:?}"
);
}
#[test]
fn test_env_var_fallback() {
let dir = tempfile::tempdir().unwrap();
let absent = dir.path().join("missing.toml");
let loaded = KhiveConfig::load(Some(&absent)).unwrap();
assert!(loaded.is_none());
let primary = "all-minilm-l6-v2".to_string();
let additional = vec!["paraphrase-multilingual-minilm-l12-v2".to_string()];
let mut engines = vec![EngineConfig {
name: "default".to_string(),
model: primary,
default: true,
fusion_weight: None,
dims: None,
}];
for (i, model) in additional.into_iter().enumerate() {
engines.push(EngineConfig {
name: format!("engine-{}", i + 1),
model,
default: false,
fusion_weight: None,
dims: None,
});
}
let cfg = KhiveConfig {
engines,
..KhiveConfig::default()
};
cfg.validate().expect("env-derived config should be valid");
assert_eq!(cfg.engines.len(), 2);
assert!(cfg.default_engine().is_some());
assert_eq!(cfg.default_engine().unwrap().name, "default");
}
#[test]
fn test_file_overrides_env() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[engines]]
name = "file-engine"
model = "all-minilm-l6-v2"
default = true
"#,
);
let cfg = KhiveConfig::load(Some(&path))
.expect("load should succeed")
.expect("file should be present");
assert_eq!(cfg.engines[0].name, "file-engine");
}
#[test]
fn test_duplicate_engine_names_rejected() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[engines]]
name = "shared"
model = "all-minilm-l6-v2"
default = true
[[engines]]
name = "shared"
model = "paraphrase-multilingual-minilm-l12-v2"
"#,
);
let err = KhiveConfig::load(Some(&path)).expect_err("should fail with duplicate name");
assert!(
matches!(config_error_root(&err), ConfigError::DuplicateName { .. }),
"expected DuplicateName, got {err:?}"
);
}
#[test]
fn test_empty_config_is_valid() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(&dir, "# no engines\n");
let cfg = KhiveConfig::load(Some(&path))
.expect("load should succeed")
.expect("file should be found");
assert!(cfg.engines.is_empty());
cfg.validate().expect("empty config should be valid");
}
#[test]
fn runtime_blob_hydration_budget_parses_and_resolves_before_engine_early_return() {
use crate::runtime::runtime_config_from_khive_config;
use crate::RuntimeConfig;
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[runtime]
blob_hydration_bytes = 134217728
"#,
);
let cfg = KhiveConfig::load(Some(&path))
.expect("load should succeed")
.expect("file should be found");
assert_eq!(cfg.runtime.blob_hydration_bytes, Some(134_217_728));
let resolved = runtime_config_from_khive_config(&cfg, RuntimeConfig::default());
assert_eq!(resolved.blob_hydration_bytes, 134_217_728);
}
#[test]
fn runtime_blob_hydration_budget_below_one_whole_blob_is_rejected() {
let dir = tempfile::tempdir().unwrap();
let value = khive_storage::MAX_BLOB_WHOLE_BYTES - 1;
let path = write_toml(
&dir,
&format!("[runtime]\nblob_hydration_bytes = {value}\n"),
);
let err = KhiveConfig::load(Some(&path)).expect_err("undersized budget must fail closed");
assert!(
matches!(
config_error_root(&err),
ConfigError::InvalidBlobHydrationBytes {
value: actual,
min,
..
} if *actual == value && *min == khive_storage::MAX_BLOB_WHOLE_BYTES
),
"got {err:?}"
);
}
#[test]
fn runtime_blob_hydration_budget_accepts_the_inclusive_portable_minimum() {
let dir = tempfile::tempdir().unwrap();
let value = khive_storage::MAX_BLOB_WHOLE_BYTES;
let path = write_toml(
&dir,
&format!("[runtime]\nblob_hydration_bytes = {value}\n"),
);
let cfg = KhiveConfig::load(Some(&path))
.expect("the inclusive minimum must be valid")
.expect("config should exist");
assert_eq!(cfg.runtime.blob_hydration_bytes, Some(value));
}
#[test]
fn runtime_blob_hydration_budget_above_semaphore_capacity_is_rejected() {
let dir = tempfile::tempdir().unwrap();
let max = tokio::sync::Semaphore::MAX_PERMITS as u64;
let value = max
.checked_add(1)
.expect("tokio maximum fits below u64::MAX");
let path = write_toml(
&dir,
&format!("[runtime]\nblob_hydration_bytes = {value}\n"),
);
let err = KhiveConfig::load(Some(&path)).expect_err("oversized budget must fail closed");
assert!(
matches!(
config_error_root(&err),
ConfigError::InvalidBlobHydrationBytes {
value: actual,
max: actual_max,
..
} if *actual == value && *actual_max == max
),
"got {err:?}"
);
}
#[test]
fn configured_fusion_weight_is_refused_instead_of_ignored() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[engines]]
name = "primary"
model = "all-minilm-l6-v2"
default = true
fusion_weight = 0.7
[[engines]]
name = "secondary"
model = "paraphrase-multilingual-minilm-l12-v2"
fusion_weight = 0.3
"#,
);
let err = KhiveConfig::load(Some(&path))
.expect_err("an explicit fusion weight must not be silently ignored");
assert!(
matches!(
config_error_root(&err),
ConfigError::UnsupportedFusionWeight { name } if name == "primary"
),
"expected UnsupportedFusionWeight for primary, got {err:?}"
);
let unweighted_path = write_toml(
&dir,
r#"
[[engines]]
name = "primary"
model = "all-minilm-l6-v2"
default = true
[[engines]]
name = "secondary"
model = "paraphrase-multilingual-minilm-l12-v2"
"#,
);
let cfg = KhiveConfig::load(Some(&unweighted_path))
.expect("unweighted multi-engine config remains valid")
.expect("file should be found");
assert_eq!(cfg.engines.len(), 2);
assert!(cfg
.engines
.iter()
.all(|engine| engine.fusion_weight.is_none()));
}
#[test]
fn test_actor_id_parsed() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[actor]
id = "lambda:khive"
display_name = "example actor"
"#,
);
let cfg = KhiveConfig::load(Some(&path))
.expect("load should succeed")
.expect("file should be found");
assert_eq!(cfg.actor.id.as_deref(), Some("lambda:khive"));
assert_eq!(cfg.actor.display_name.as_deref(), Some("example actor"));
assert!(cfg.engines.is_empty());
}
#[test]
fn gate_mailbox_reader_config_loads_exact_labels_and_rejects_bad_policy() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
"[actor]\nid = \"lambda:owner\"\nmailbox_readers = [\"lambda:helper\", \"助手/审阅者\", \"lambda:helper\"]\n",
);
let config = KhiveConfig::load(Some(&path)).unwrap().unwrap();
assert_eq!(
config.actor.mailbox_readers,
["lambda:helper", "助手/审阅者", "lambda:helper"]
);
for (owner, readers) in [
(None, vec!["reader".to_string()]),
(Some("local"), vec!["reader".to_string()]),
(Some("lambda:owner"), vec!["local".to_string()]),
(Some("lambda:owner"), vec![String::new()]),
(Some("lambda:owner"), vec![" \t".to_string()]),
(Some("lambda:owner"), vec!["bad\nactor".to_string()]),
(Some("lambda:owner"), vec!["x".repeat(256)]),
(Some("lambda:owner"), vec!["reader".to_string(); 257]),
] {
let config = KhiveConfig {
actor: ActorConfig {
id: owner.map(str::to_string),
mailbox_readers: readers,
..Default::default()
},
..Default::default()
};
assert!(matches!(
config.validate(),
Err(ConfigError::InvalidMailboxReaders { .. })
));
}
for source in [
"[actor]\nmailbox_readers = [\"reader\"]\n",
"[actor]\nid = \"local\"\nmailbox_readers = [\"reader\"]\n",
"[actor]\nid = \"lambda:owner\"\nmailbox_readers = [\"\"]\n",
"[actor]\nid = \"lambda:owner\"\nmailbox_readers = \"reader\"\n",
] {
let path = write_toml(&dir, source);
assert!(KhiveConfig::load(Some(&path)).is_err());
}
let boundary = KhiveConfig {
actor: ActorConfig {
id: Some("lambda:owner".into()),
mailbox_readers: vec!["x".repeat(255); 256],
..Default::default()
},
..Default::default()
};
boundary.validate().unwrap();
KhiveConfig::default().validate().unwrap();
}
#[test]
fn test_actor_and_engines_together() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[actor]
id = "lambda:test"
[[engines]]
name = "default"
model = "all-minilm-l6-v2"
default = true
"#,
);
let cfg = KhiveConfig::load(Some(&path))
.expect("load should succeed")
.expect("file should be found");
assert_eq!(cfg.actor.id.as_deref(), Some("lambda:test"));
assert_eq!(cfg.engines.len(), 1);
}
#[test]
fn test_actor_absent_defaults_to_none() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[engines]]
name = "x"
model = "all-minilm-l6-v2"
default = true
"#,
);
let cfg = KhiveConfig::load(Some(&path))
.expect("load should succeed")
.expect("file should be found");
assert!(
cfg.actor.id.is_none(),
"actor.id must be None when [actor] section is absent"
);
}
#[test]
fn test_load_with_home_fallback_no_files() {
let project_dir = tempfile::tempdir().unwrap();
let home_dir = tempfile::tempdir().unwrap();
let result = KhiveConfig::load_with_roots(project_dir.path(), Some(home_dir.path()), None);
assert!(
result.expect("no error expected").is_none(),
"should return None when no config files exist in the given roots"
);
}
#[test]
fn home_gate_config_loads_while_explicit_empty_config_is_hermetic() {
let project_dir = tempfile::tempdir().unwrap();
let home_dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(home_dir.path().join(".khive")).unwrap();
std::fs::write(
home_dir.path().join(".khive/config.toml"),
"[gate]\ngranted_actors = [\"lambda:enrolled\"]\ndeny_writes_for = [\"*:duty\"]\n",
)
.unwrap();
let loaded = KhiveConfig::load_with_roots(project_dir.path(), Some(home_dir.path()), None)
.expect("supported home gate policy loads")
.expect("home config exists");
let gate = loaded.gate.expect("gate table");
assert_eq!(gate.granted_actors, vec!["lambda:enrolled"]);
assert_eq!(gate.deny_writes_for, vec!["*:duty"]);
let empty = project_dir.path().join("empty-khive-config.toml");
std::fs::write(&empty, "").unwrap();
let isolated = KhiveConfig::load_with_home_fallback(Some(&empty), None)
.expect("an explicit empty fixture must isolate config discovery")
.expect("the explicit config exists");
assert!(isolated.engines.is_empty());
assert!(isolated.actor.id.is_none());
assert!(isolated.gate.is_none());
}
#[test]
fn test_load_with_home_fallback_explicit_path() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[actor]
id = "lambda:explicit"
"#,
);
let cfg = KhiveConfig::load_with_home_fallback(Some(&path), None)
.expect("no error expected")
.expect("file found");
assert_eq!(cfg.actor.id.as_deref(), Some("lambda:explicit"));
}
#[test]
fn load_with_home_fallback_and_source_names_selected_file() {
let project_dir = tempfile::tempdir().unwrap();
let home_dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(home_dir.path().join(".khive")).unwrap();
let selected = home_dir.path().join(".khive/config.toml");
std::fs::write(&selected, "[actor]\nid = \"lambda:home\"\n").unwrap();
let (config, source) = KhiveConfig::load_with_roots_and_source(
project_dir.path(),
Some(home_dir.path()),
None,
)
.expect("load should succeed")
.expect("home fallback should be selected");
assert_eq!(config.actor.id.as_deref(), Some("lambda:home"));
assert_eq!(
source,
std::fs::canonicalize(selected).expect("canonical selected config path")
);
}
#[test]
fn test_invalid_actor_id_rejected_at_load() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[actor]
id = "bad namespace"
"#,
);
let err = KhiveConfig::load(Some(&path)).expect_err("should fail with invalid actor.id");
assert!(
matches!(config_error_root(&err), ConfigError::InvalidActorId { .. }),
"expected InvalidActorId, got {err:?}"
);
}
#[test]
fn test_empty_actor_id_rejected() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[actor]
id = ""
"#,
);
let err = KhiveConfig::load(Some(&path)).expect_err("empty actor.id should be rejected");
assert!(
matches!(config_error_root(&err), ConfigError::InvalidActorId { .. }),
"expected InvalidActorId for empty string, got {err:?}"
);
}
#[test]
fn test_malformed_actor_id_lambda_colon_only() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[actor]
id = "lambda:"
"#,
);
let err =
KhiveConfig::load(Some(&path)).expect_err("lambda: with no slug should be rejected");
assert!(
matches!(config_error_root(&err), ConfigError::InvalidActorId { .. }),
"expected InvalidActorId for 'lambda:', got {err:?}"
);
}
#[test]
fn test_runtime_config_actor_id_does_not_override_namespace() {
use crate::runtime::runtime_config_from_khive_config;
use crate::RuntimeConfig;
use khive_types::namespace::Namespace;
let cfg = KhiveConfig {
engines: vec![],
actor: ActorConfig {
id: Some("lambda:test-actor".to_string()),
display_name: None,
..Default::default()
},
..KhiveConfig::default()
};
cfg.validate().expect("valid config");
let base = RuntimeConfig::default();
let result = runtime_config_from_khive_config(&cfg, base);
assert_eq!(
result.default_namespace,
Namespace::local(),
"actor.id must NOT become default_namespace (ADR-007 Rev 4 Rule 0); \
writes stay pinned to local"
);
assert!(
result
.visible_namespaces
.contains(&Namespace::parse("lambda:test-actor").unwrap()),
"actor.id must be folded into visible_namespaces (ADR-007 Rev 4 Rule 3b fold-in); \
got: {:?}",
result.visible_namespaces
);
}
#[test]
fn test_runtime_config_no_actor_preserves_base() {
use crate::runtime::runtime_config_from_khive_config;
use crate::RuntimeConfig;
use khive_types::namespace::Namespace;
let cfg = KhiveConfig {
engines: vec![],
actor: ActorConfig {
id: None,
display_name: None,
..Default::default()
},
..KhiveConfig::default()
};
cfg.validate().expect("valid config");
let base_ns = Namespace::parse("lambda:base").unwrap();
let base = RuntimeConfig {
default_namespace: base_ns.clone(),
..RuntimeConfig::default()
};
let result = runtime_config_from_khive_config(&cfg, base);
assert_eq!(
result.default_namespace, base_ns,
"no actor.id must leave base namespace unchanged"
);
}
#[test]
fn test_load_with_home_fallback_project_root_over_hidden() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join(".khive")).unwrap();
std::fs::write(
dir.path().join(".khive/config.toml"),
"[actor]\nid = \"lambda:hidden\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("khive.toml"),
"[actor]\nid = \"lambda:project-root\"\n",
)
.unwrap();
let cfg = KhiveConfig::load_with_roots(dir.path(), None, None)
.expect("no error expected")
.expect("file should be found");
assert_eq!(
cfg.actor.id.as_deref(),
Some("lambda:project-root"),
"khive.toml (tier 2) must win over .khive/config.toml (tier 3)"
);
}
#[test]
fn test_load_with_home_fallback_hidden_over_absent_root() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join(".khive")).unwrap();
std::fs::write(
dir.path().join(".khive/config.toml"),
"[actor]\nid = \"lambda:hidden-config\"\n",
)
.unwrap();
let cfg = KhiveConfig::load_with_roots(dir.path(), None, None)
.expect("no error expected")
.expect("file should be found");
assert_eq!(
cfg.actor.id.as_deref(),
Some("lambda:hidden-config"),
".khive/config.toml (tier 3) must be found when khive.toml is absent"
);
}
#[test]
fn test_load_with_roots_home_tier_found() {
let project_dir = tempfile::tempdir().unwrap();
let home_dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(home_dir.path().join(".khive")).unwrap();
std::fs::write(
home_dir.path().join(".khive/config.toml"),
"[actor]\nid = \"lambda:user-global\"\n",
)
.unwrap();
let cfg = KhiveConfig::load_with_roots(project_dir.path(), Some(home_dir.path()), None)
.expect("no error expected")
.expect("file should be found");
assert_eq!(
cfg.actor.id.as_deref(),
Some("lambda:user-global"),
"~/.khive/config.toml (tier 4) must be found when project files absent"
);
}
#[test]
fn test_load_with_roots_project_wins_over_home() {
let project_dir = tempfile::tempdir().unwrap();
let home_dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(home_dir.path().join(".khive")).unwrap();
std::fs::write(
home_dir.path().join(".khive/config.toml"),
"[actor]\nid = \"lambda:user-global\"\n",
)
.unwrap();
std::fs::create_dir_all(project_dir.path().join(".khive")).unwrap();
std::fs::write(
project_dir.path().join(".khive/config.toml"),
"[actor]\nid = \"lambda:project-wins\"\n",
)
.unwrap();
let cfg = KhiveConfig::load_with_roots(project_dir.path(), Some(home_dir.path()), None)
.expect("no error expected")
.expect("file should be found");
assert_eq!(
cfg.actor.id.as_deref(),
Some("lambda:project-wins"),
"project .khive/config.toml (tier 3) must win over ~/.khive/config.toml (tier 4)"
);
}
#[test]
fn test_load_with_roots_same_db_different_cwd_resolves_identical_config() {
let cwd_a = tempfile::tempdir().unwrap();
let cwd_b = tempfile::tempdir().unwrap();
std::fs::create_dir_all(cwd_a.path().join(".khive")).unwrap();
std::fs::write(
cwd_a.path().join(".khive/config.toml"),
"[actor]\nid = \"lambda:wrong-cwd-a\"\n",
)
.unwrap();
std::fs::create_dir_all(cwd_b.path().join(".khive")).unwrap();
std::fs::write(
cwd_b.path().join(".khive/config.toml"),
"[actor]\nid = \"lambda:wrong-cwd-b\"\n",
)
.unwrap();
let db_root = tempfile::tempdir().unwrap();
let khive_dir = db_root.path().join(".khive");
std::fs::create_dir_all(&khive_dir).unwrap();
let db_path = khive_dir.join("khive.db");
std::fs::write(&db_path, b"").unwrap(); std::fs::write(
khive_dir.join("config.toml"),
"[actor]\nid = \"lambda:db-anchored\"\n",
)
.unwrap();
let cfg_a = KhiveConfig::load_with_roots(cwd_a.path(), None, Some(&db_path))
.expect("no error expected")
.expect("db-anchored config must be found from cwd A");
let cfg_b = KhiveConfig::load_with_roots(cwd_b.path(), None, Some(&db_path))
.expect("no error expected")
.expect("db-anchored config must be found from cwd B");
assert_eq!(
cfg_a.actor.id.as_deref(),
Some("lambda:db-anchored"),
"cwd A must resolve the db-anchored config, not its own decoy"
);
assert_eq!(
cfg_b.actor.id.as_deref(),
Some("lambda:db-anchored"),
"cwd B must resolve the db-anchored config, not its own decoy"
);
assert_eq!(
cfg_a.actor.id, cfg_b.actor.id,
"two processes at different cwds targeting the same db must resolve \
identical config, killing config_id drift between client and daemon"
);
}
#[test]
fn test_load_with_home_fallback_explicit_config_wins_over_db_anchor() {
let explicit_dir = tempfile::tempdir().unwrap();
let explicit_path = write_toml(&explicit_dir, "[actor]\nid = \"lambda:explicit-wins\"\n");
let db_root = tempfile::tempdir().unwrap();
let khive_dir = db_root.path().join(".khive");
std::fs::create_dir_all(&khive_dir).unwrap();
let db_path = khive_dir.join("khive.db");
std::fs::write(&db_path, b"").unwrap();
std::fs::write(
khive_dir.join("config.toml"),
"[actor]\nid = \"lambda:db-anchor-loses\"\n",
)
.unwrap();
let cfg = KhiveConfig::load_with_home_fallback(Some(&explicit_path), Some(&db_path))
.expect("no error expected")
.expect("explicit path must be found");
assert_eq!(
cfg.actor.id.as_deref(),
Some("lambda:explicit-wins"),
"explicit --config/KHIVE_CONFIG must win over the db-dir anchor"
);
}
#[test]
fn test_load_with_roots_home_fallback_reached_when_db_anchor_has_no_config() {
let cwd = tempfile::tempdir().unwrap();
let home_dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(home_dir.path().join(".khive")).unwrap();
std::fs::write(
home_dir.path().join(".khive/config.toml"),
"[actor]\nid = \"lambda:home-fallback\"\n",
)
.unwrap();
let db_root = tempfile::tempdir().unwrap();
let khive_dir = db_root.path().join(".khive");
std::fs::create_dir_all(&khive_dir).unwrap();
let db_path = khive_dir.join("khive.db");
std::fs::write(&db_path, b"").unwrap();
let cfg = KhiveConfig::load_with_roots(cwd.path(), Some(home_dir.path()), Some(&db_path))
.expect("no error expected")
.expect("home-tier config must be found");
assert_eq!(
cfg.actor.id.as_deref(),
Some("lambda:home-fallback"),
"tier 4 (~/.khive/config.toml) must still be reached when the db-anchored \
tier-3 directory has no config.toml"
);
}
#[test]
fn test_load_with_roots_nonexistent_db_path_does_not_panic_and_falls_through() {
let cwd = tempfile::tempdir().unwrap();
let home_dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(home_dir.path().join(".khive")).unwrap();
std::fs::write(
home_dir.path().join(".khive/config.toml"),
"[actor]\nid = \"lambda:home-cold-start\"\n",
)
.unwrap();
let nonexistent_db = cwd.path().join("never-created/.khive/khive.db");
let cfg =
KhiveConfig::load_with_roots(cwd.path(), Some(home_dir.path()), Some(&nonexistent_db))
.expect("cold-start db path must not error or panic")
.expect("home-tier config must still be found");
assert_eq!(
cfg.actor.id.as_deref(),
Some("lambda:home-cold-start"),
"a nonexistent db path (cold start) must fall through to tier 4, not panic"
);
}
#[test]
fn test_load_with_roots_relative_nonexistent_db_path_does_not_panic() {
let cwd = tempfile::tempdir().unwrap();
let relative_db = PathBuf::from("never-created/.khive/khive.db");
let result = KhiveConfig::load_with_roots(cwd.path(), None, Some(&relative_db));
assert!(
result.is_ok(),
"relative cold-start db path must not error or panic: {result:?}"
);
assert!(
result.unwrap().is_none(),
"no config exists anywhere in this test; result must be None"
);
}
#[test]
fn test_no_backends_section_is_valid() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[engines]]
name = "default"
model = "all-minilm-l6-v2"
default = true
"#,
);
let cfg = KhiveConfig::load(Some(&path))
.expect("no error")
.expect("file found");
assert!(cfg.backends.is_empty());
assert!(cfg.packs.is_empty());
}
#[test]
fn test_single_sqlite_backend_parses() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[backends]]
name = "knowledge"
kind = "sqlite"
path = "/tmp/knowledge.db"
"#,
);
let cfg = KhiveConfig::load(Some(&path))
.expect("no error")
.expect("file found");
assert_eq!(cfg.backends.len(), 1);
let b = &cfg.backends[0];
assert_eq!(b.name, "knowledge");
assert!(matches!(b.kind, BackendKind::Sqlite));
assert_eq!(
b.path.as_ref().and_then(|p| p.to_str()),
Some("/tmp/knowledge.db")
);
}
#[test]
fn test_memory_backend_parses() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[backends]]
name = "ephemeral"
kind = "memory"
"#,
);
let cfg = KhiveConfig::load(Some(&path))
.expect("no error")
.expect("file found");
assert_eq!(cfg.backends.len(), 1);
assert!(matches!(cfg.backends[0].kind, BackendKind::Memory));
}
#[test]
fn memory_wal_policy_ignores_environment_but_keeps_its_own_field() {
for raw in ["8192", "abc"] {
let resolved = resolve_wal_ceiling(
None,
Some(raw),
"ephemeral",
BackendKind::Memory,
true,
false,
)
.unwrap();
assert_eq!(resolved.configured_bytes, 0, "MEMORY_IGNORES_ENVIRONMENT");
assert_eq!(resolved.source, khive_db::WalCeilingSource::Default);
let error = resolve_wal_ceiling(
Some(8192),
Some(raw),
"ephemeral",
BackendKind::Memory,
true,
false,
)
.expect_err("DECLARED_MEMORY_FIELD_REFUSAL");
assert!(matches!(
error,
ConfigError::WalCeilingMemoryBackend { value: 8192, .. }
));
}
let error = resolve_wal_ceiling(
None,
Some("credential-secret-marker"),
"file",
BackendKind::Sqlite,
true,
false,
)
.unwrap_err();
assert_eq!(
error.to_string(),
"KHIVE_SQLITE_WAL_CEILING_BYTES must be an unsigned decimal byte count",
"RAW_WAL_ENVIRONMENT_NOT_ECHOED"
);
}
#[test]
fn wal_ceiling_nonzero_memory_backend_fails_config_load() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
"[[backends]]\nname = 'main'\nkind = 'memory'\nwal_ceiling_bytes = 4152\n",
);
let error = KhiveConfig::load(Some(&path)).expect_err("memory has no WAL extent");
assert!(matches!(
config_error_root(&error),
ConfigError::WalCeilingMemoryBackend { name, value }
if name == "main" && *value == 4152
));
}
#[test]
fn wal_ceiling_nonzero_non_wal_backend_is_typed_error() {
let error =
resolve_wal_ceiling(Some(4152), None, "main", BackendKind::Sqlite, false, false)
.expect_err("non-WAL SQLite cannot enforce a WAL extent ceiling");
assert!(matches!(
error,
ConfigError::WalCeilingNonWalBackend { name, value }
if name == "main" && value == 4152
));
}
#[test]
fn wal_ceiling_offset_overflow_fails_before_backend_kind() {
let overflow = i64::MAX as u64 + 1;
let error = resolve_wal_ceiling(
Some(overflow),
None,
"ephemeral",
BackendKind::Memory,
false,
false,
)
.expect_err("unsupported SQLite offset must fail before backend checks");
assert!(matches!(
error,
ConfigError::WalCeilingOffsetOverflow { name, value }
if name == "ephemeral" && value == overflow
));
}
#[test]
fn wal_ceiling_field_precedes_environment_and_read_only_disables_enforcement() {
let resolved = resolve_wal_ceiling(
Some(4152),
Some("not-a-byte-count"),
"archive",
BackendKind::Sqlite,
true,
true,
)
.expect("higher-priority field makes lower-priority environment irrelevant");
assert_eq!(resolved.configured_bytes, 4152);
assert_eq!(resolved.effective_bytes, 0);
assert_eq!(resolved.source, khive_db::WalCeilingSource::BackendField);
let invalid = resolve_wal_ceiling(
None,
Some("not-a-byte-count"),
"archive",
BackendKind::Sqlite,
true,
false,
)
.expect_err("a selected malformed environment must fail closed");
assert!(matches!(
invalid,
ConfigError::InvalidWalCeilingEnvironment { .. }
));
}
#[test]
fn test_pack_backend_assignment_parses() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[backends]]
name = "knowledge"
kind = "memory"
[packs.knowledge]
backend = "knowledge"
"#,
);
let cfg = KhiveConfig::load(Some(&path))
.expect("no error")
.expect("file found");
assert_eq!(cfg.packs.len(), 1);
let pc = cfg.packs.get("knowledge").expect("knowledge pack present");
assert_eq!(pc.backend, "knowledge");
}
#[test]
fn test_duplicate_backend_name_rejected() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[backends]]
name = "dup"
kind = "memory"
[[backends]]
name = "dup"
kind = "memory"
"#,
);
let err = KhiveConfig::load(Some(&path)).expect_err("should fail with duplicate name");
assert!(
matches!(config_error_root(&err), ConfigError::DuplicateBackendName { ref name } if name == "dup"),
"expected DuplicateBackendName {{ name: \"dup\" }}, got {err:?}"
);
}
#[test]
fn test_empty_backend_name_rejected() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[backends]]
name = ""
kind = "memory"
"#,
);
let err = KhiveConfig::load(Some(&path)).expect_err("empty backend name must fail");
assert!(
matches!(config_error_root(&err), ConfigError::InvalidBackendName { ref name, .. } if name.is_empty()),
"expected InvalidBackendName for the empty name, got {err:?}"
);
}
#[test]
fn test_backend_served_kinds_absent_and_declared() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[backends]]
name = "legacy"
kind = "memory"
[[backends]]
name = "notes"
kind = "memory"
served_kinds = ["note", "event"]
"#,
);
let config = KhiveConfig::load(Some(&path))
.expect("valid served-kind declarations")
.expect("config file found");
assert!(config.backends[0].served_kinds.is_none());
assert_eq!(
config.backends[1].served_kinds,
Some(BTreeSet::from([SubstrateKind::Note, SubstrateKind::Event]))
);
}
#[test]
fn test_empty_backend_served_kinds_rejected() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[backends]]
name = "main"
kind = "memory"
served_kinds = []
"#,
);
let error = KhiveConfig::load(Some(&path))
.expect_err("an explicit empty served-kind declaration must fail closed");
assert!(matches!(
config_error_root(&error),
ConfigError::EmptyBackendServedKinds { name } if name == "main"
));
}
#[test]
fn test_unknown_backend_served_kind_rejected() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[backends]]
name = "main"
kind = "memory"
served_kinds = ["asset"]
"#,
);
let error = KhiveConfig::load(Some(&path))
.expect_err("served-kind declarations use a closed vocabulary");
assert!(matches!(
config_error_root(&error),
ConfigError::Parse { .. }
));
assert!(error.to_string().contains("unknown variant `asset`"));
}
#[test]
fn test_pack_referencing_undefined_backend_rejected() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[backends]]
name = "knowledge"
kind = "memory"
[packs.kg]
backend = "nonexistent"
"#,
);
let err =
KhiveConfig::load(Some(&path)).expect_err("should fail with unknown backend reference");
assert!(
matches!(config_error_root(&err), ConfigError::UnknownPackBackend { ref pack, ref backend, .. }
if pack == "kg" && backend == "nonexistent"),
"expected UnknownPackBackend for kg→nonexistent, got {err:?}"
);
}
#[test]
fn test_pack_config_without_backends_section_is_allowed() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[packs.kg]
backend = "main"
"#,
);
let cfg = KhiveConfig::load(Some(&path))
.expect("no error expected")
.expect("file found");
assert_eq!(cfg.backends.len(), 0);
assert_eq!(cfg.packs.len(), 1);
}
#[test]
fn test_implicit_main_rejects_unknown_pack_backend() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(&dir, "[packs.comm]\nbackend = 'does-not-exist'\n");
let error = KhiveConfig::load(Some(&path)).expect_err("unknown route must fail");
assert!(matches!(
config_error_root(&error),
ConfigError::UnknownPackBackend { pack, backend, defined }
if pack == "comm" && backend == "does-not-exist" && defined == "main"
));
}
#[test]
fn test_backend_search_coverage_rejects_missing_substrates() {
for (served, missing) in [
("'note'", vec![SubstrateKind::Entity]),
("'entity'", vec![SubstrateKind::Note]),
("'event'", vec![SubstrateKind::Note, SubstrateKind::Entity]),
] {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
&format!(
"[[backends]]\nname = 'main'\nkind = 'memory'\nserved_kinds = [{served}]\n"
),
);
let error = KhiveConfig::load(Some(&path)).expect_err("incomplete coverage");
assert!(
matches!(
config_error_root(&error),
ConfigError::MissingBackendSearchKinds { kinds, defined }
if kinds == &missing && defined == "main"
),
"unexpected coverage error: {error}"
);
}
}
#[test]
fn test_backend_search_coverage_allows_split_substrates_and_event_only_secondary() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[backends]]
name = "main"
kind = "memory"
served_kinds = ["entity"]
[[backends]]
name = "notes"
kind = "memory"
served_kinds = ["note"]
[[backends]]
name = "events"
kind = "memory"
served_kinds = ["event"]
"#,
);
KhiveConfig::load(Some(&path)).expect("search coverage is the union of backends");
}
#[test]
fn test_backend_cache_mb_rejected_at_validate() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[backends]]
name = "main"
kind = "memory"
cache_mb = 128
"#,
);
let err = KhiveConfig::load(Some(&path)).expect_err("cache_mb must be rejected");
assert!(
matches!(config_error_root(&err), ConfigError::UnsupportedBackendField { ref name, field: "cache_mb" } if name == "main"),
"expected UnsupportedBackendField {{ name: \"main\", field: \"cache_mb\" }}, got {err:?}"
);
}
#[test]
fn test_backend_journal_mode_rejected_at_validate() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[backends]]
name = "main"
kind = "memory"
journal_mode = "wal"
"#,
);
let err = KhiveConfig::load(Some(&path)).expect_err("journal_mode must be rejected");
assert!(
matches!(config_error_root(&err), ConfigError::UnsupportedBackendField { ref name, field: "journal_mode" } if name == "main"),
"expected UnsupportedBackendField {{ name: \"main\", field: \"journal_mode\" }}, got {err:?}"
);
}
#[test]
fn test_top_level_db_rejected_at_validate() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
db = "/tmp/scratch/demo.db"
"#,
);
let err = KhiveConfig::load(Some(&path)).expect_err("top-level db must be rejected");
assert!(
matches!(config_error_root(&err), ConfigError::UnsupportedTopLevelDb { ref value } if value == "/tmp/scratch/demo.db"),
"expected UnsupportedTopLevelDb {{ value: \"/tmp/scratch/demo.db\" }}, got {err:?}"
);
}
#[test]
fn gate_caller_enrollment_config_loads_for_runtime_enforcement() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[gate]
granted_actors = ["lambda:enrolled"]
grant_unattributed = false
"#,
);
let config = KhiveConfig::load(Some(&path))
.expect("the supported caller-enrollment policy must parse")
.expect("config exists");
let gate = config.gate.expect("gate section");
assert_eq!(gate.granted_actors, vec!["lambda:enrolled"]);
assert!(!gate.grant_unattributed);
}
#[test]
fn unknown_actor_key_fails_to_load() {
let dir = tempfile::tempdir().unwrap();
let supported = write_toml(
&dir,
r#"
[actor]
id = "lambda:example"
visible_namespaces = ["lambda:other"]
"#,
);
KhiveConfig::load(Some(&supported))
.expect("a config using only supported [actor] keys must parse")
.expect("config exists");
let misplaced = write_toml(
&dir,
r#"
[actor]
id = "lambda:example"
grant_unattributed = false
"#,
);
let err = KhiveConfig::load(Some(&misplaced))
.expect_err("a [gate] key written under [actor] must fail startup");
assert!(
err.to_string().contains("grant_unattributed"),
"the refusal must name the offending key, got: {err}"
);
}
#[test]
fn caller_enrollment_policy_is_enforced_at_authorization() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[actor]
id = "lambda:enrolled"
[gate]
granted_actors = ["lambda:enrolled"]
grant_unattributed = false
"#,
);
let mut config = KhiveConfig::load(Some(&path))
.expect("load")
.expect("config exists");
let allowed = crate::runtime_config_from_khive_config(&config, in_memory_runtime_config());
let runtime = crate::KhiveRuntime::new(allowed).expect("runtime");
runtime
.authorize(Namespace::local())
.expect("listed actor is admitted");
config.actor.id = Some("lambda:other".to_string());
let denied = crate::runtime_config_from_khive_config(&config, in_memory_runtime_config());
let runtime = crate::KhiveRuntime::new(denied).expect("runtime");
assert!(matches!(
runtime.authorize(Namespace::local()),
Err(crate::RuntimeError::PermissionDenied { ref verb, ref reason, .. })
if verb == "authorize" && reason == "actor is not enrolled"
));
}
#[test]
fn grant_unattributed_controls_anonymous_authorization() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(&dir, "[gate]\ngrant_unattributed = false\n");
let mut config = KhiveConfig::load(Some(&path))
.expect("load")
.expect("config exists");
let denied = crate::runtime_config_from_khive_config(&config, in_memory_runtime_config());
let runtime = crate::KhiveRuntime::new(denied).expect("runtime");
assert!(matches!(
runtime.authorize(Namespace::local()),
Err(crate::RuntimeError::PermissionDenied { ref reason, .. })
if reason == "unattributed caller is not enrolled"
));
config.gate.as_mut().expect("gate").grant_unattributed = true;
let allowed = crate::runtime_config_from_khive_config(&config, in_memory_runtime_config());
crate::KhiveRuntime::new(allowed)
.expect("runtime")
.authorize(Namespace::local())
.expect("anonymous caller is explicitly admitted");
}
#[test]
fn empty_gate_table_is_explicit_deny_all_policy() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(&dir, "[gate]\n");
let config = KhiveConfig::load(Some(&path))
.expect("empty gate table parses")
.expect("config exists");
assert_eq!(config.gate, Some(GateSectionConfig::default()));
let runtime_config =
crate::runtime_config_from_khive_config(&config, in_memory_runtime_config());
let runtime = crate::KhiveRuntime::new(runtime_config).expect("runtime");
assert!(matches!(
runtime.authorize(Namespace::local()),
Err(crate::RuntimeError::PermissionDenied { .. })
));
}
#[test]
fn unknown_gate_key_fails_startup() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(&dir, "[gate]\ngranted_actor = [\"lambda:typo\"]\n");
let err = KhiveConfig::load(Some(&path)).expect_err("unknown gate key must fail");
assert!(matches!(err, ConfigError::Parse { .. }));
assert!(err.to_string().contains("unknown field"), "{err}");
}
#[test]
fn write_denials_survive_both_runtime_config_paths() {
let dir = tempfile::tempdir().unwrap();
for engines in [
"",
"\n[[engines]]\nname = 'main'\nmodel = 'all-minilm-l6-v2'\ndefault = true\n",
] {
let path = write_toml(&dir, &format!(
"[actor]\nid='seat:duty'\n[gate]\ngranted_actors=['seat:duty','seat:writer']\ndeny_writes_for=['*:duty']\n{engines}"
));
let config = KhiveConfig::load(Some(&path)).unwrap().unwrap();
let runtime =
crate::runtime_config_from_khive_config(&config, in_memory_runtime_config());
for (actor, verb, allowed) in [
("seat:duty", "list", true),
("seat:duty", "create", false),
("seat:writer", "create", true),
("unlisted", "list", false),
] {
let req = crate::GateRequest::new(
crate::ActorRef::new("actor", actor),
Namespace::local(),
verb,
serde_json::Value::Null,
);
assert_eq!(
runtime.gate.check(&req).unwrap().is_allow(),
allowed,
"{actor} {verb}"
);
}
}
}
#[test]
fn invalid_write_denials_fail_config_load_and_direct_config_fails_closed() {
let dir = tempfile::tempdir().unwrap();
for value in [
"['']".to_string(),
"[' ']".into(),
format!("['{}']", "é".repeat(129)),
format!("[{}]", vec!["'*'"; 257].join(",")),
] {
let path = write_toml(&dir, &format!("[gate]\ndeny_writes_for={value}\n"));
let error = KhiveConfig::load(Some(&path)).unwrap_err();
assert!(
matches!(
config_error_root(&error),
ConfigError::InvalidWriteDenyPatterns { .. }
),
"{error}"
);
}
for field in ["deny_write_for=['*']", "deny_writes_for=[17]"] {
let path = write_toml(&dir, &format!("[gate]\n{field}\n"));
assert!(KhiveConfig::load(Some(&path)).is_err());
}
let path = write_toml(
&dir,
"[gate]\ngranted_actors=['writer']\ndeny_writes_for=['用户@*/[?]']\n",
);
let mut config = KhiveConfig::load(Some(&path)).unwrap().unwrap();
config.gate.as_mut().unwrap().deny_writes_for = vec![String::new()];
let runtime = crate::runtime_config_from_khive_config(&config, in_memory_runtime_config());
let req = crate::GateRequest::new(
crate::ActorRef::new("actor", "writer"),
Namespace::local(),
"list",
serde_json::Value::Null,
);
assert!(matches!(
runtime.gate.check(&req),
Err(crate::GateError::Policy(_))
));
}
#[test]
fn absent_gate_preserves_the_programmatic_gate() {
let mut base = in_memory_runtime_config();
base.gate = std::sync::Arc::new(crate::CallerEnrollmentGate::new(vec![], false));
let configured =
crate::runtime_config_from_khive_config(&KhiveConfig::default(), base.clone());
assert!(std::sync::Arc::ptr_eq(&base.gate, &configured.gate));
}
#[test]
fn invalid_granted_actor_fails_startup() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(&dir, "[gate]\ngranted_actors = [\"not valid\"]\n");
let err = KhiveConfig::load(Some(&path)).expect_err("invalid actor id must fail");
assert!(matches!(
config_error_root(&err),
ConfigError::InvalidGrantedActorId { id, .. } if id == "not valid"
));
}
#[test]
fn unrelated_unknown_top_level_sections_remain_forward_compatible() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(&dir, "[future_feature]\nenabled = true\n");
KhiveConfig::load(Some(&path))
.expect("unrelated future config stays forward compatible")
.expect("config exists");
}
#[test]
fn brain_fleet_readers_default_to_empty() {
assert!(KhiveConfig::default().brain.fleet_readers.is_empty());
assert!(in_memory_runtime_config().brain.fleet_readers.is_empty());
let dir = tempfile::tempdir().unwrap();
for engines in [
"",
"[[engines]]\nname = \"primary\"\nmodel = \"all-minilm-l6-v2\"\ndefault = true\n",
] {
for brain in ["", "[brain]\n", "[brain]\nfleet_readers = []\n"] {
let path = write_toml(&dir, &format!("{engines}\n{brain}"));
let config = KhiveConfig::load(Some(&path))
.expect("load")
.expect("config exists");
assert!(config.brain.fleet_readers.is_empty());
let mut base = in_memory_runtime_config();
base.brain.fleet_readers = vec!["lambda:previous".to_string()];
let resolved = crate::runtime_config_from_khive_config(&config, base);
assert!(resolved.brain.fleet_readers.is_empty());
}
}
}
#[test]
fn brain_fleet_readers_parse_and_resolve_with_or_without_engines() {
let dir = tempfile::tempdir().unwrap();
for engines in [
"",
"[[engines]]\nname = \"primary\"\nmodel = \"all-minilm-l6-v2\"\ndefault = true\n",
] {
let path = write_toml(
&dir,
&format!(
"{engines}\n[brain]\nfleet_readers = [\"lambda:reader\", \"lambda:auditor\"]\n"
),
);
let config = KhiveConfig::load(Some(&path))
.expect("load")
.expect("config exists");
assert_eq!(
config.brain.fleet_readers,
vec!["lambda:reader", "lambda:auditor"]
);
let mut base = in_memory_runtime_config();
base.brain.fleet_readers = vec!["lambda:previous".to_string()];
let resolved = crate::runtime_config_from_khive_config(&config, base);
assert_eq!(
resolved.brain.fleet_readers,
vec!["lambda:reader", "lambda:auditor"]
);
}
}
#[test]
fn unknown_brain_key_fails_startup() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(&dir, "[brain]\nfleet_reader = [\"lambda:reader\"]\n");
let err = KhiveConfig::load(Some(&path)).expect_err("unknown brain key must fail");
assert!(matches!(err, ConfigError::Parse { .. }));
assert!(err.to_string().contains("unknown field"), "{err}");
}
#[test]
fn telemetry_missing_default_stays_absent_with_or_without_engines() {
use crate::{TelemetryCarrier, TelemetryConfig};
assert_eq!(KhiveConfig::default().telemetry, TelemetryConfig::default());
assert_eq!(
in_memory_runtime_config().telemetry,
TelemetryConfig::default()
);
let dir = tempfile::tempdir().unwrap();
for engines in [
"",
"[[engines]]\nname = \"primary\"\nmodel = \"all-minilm-l6-v2\"\ndefault = true\n",
] {
for telemetry in ["", "[telemetry]\n"] {
let path = write_toml(&dir, &format!("{engines}\n{telemetry}"));
let config = KhiveConfig::load(Some(&path)).unwrap().unwrap();
let mut base = in_memory_runtime_config();
base.telemetry.stream = "previous".to_string();
base.telemetry.default_carrier = Some(TelemetryCarrier::Durable);
let resolved = crate::runtime_config_from_khive_config(&config, base);
assert_eq!(resolved.telemetry, TelemetryConfig::default());
assert_eq!(resolved.telemetry.stream, "telemetry");
assert_eq!(resolved.telemetry.default_carrier, None);
let error = resolved
.telemetry
.validate_activation()
.expect_err("activating telemetry requires the declared default");
assert!(error.to_string().contains("telemetry.default_carrier"));
}
}
}
#[test]
fn telemetry_table_loads_and_resolves_with_or_without_engines() {
use crate::{TelemetryCarrier, TelemetryFailurePosture};
let dir = tempfile::tempdir().unwrap();
for engines in [
"",
"[[engines]]\nname = \"primary\"\nmodel = \"all-minilm-l6-v2\"\ndefault = true\n",
] {
let path = write_toml(
&dir,
&format!(
r#"{engines}
[telemetry]
stream = "operations"
default_carrier = "durable"
[[telemetry.channels]]
kinds = ["run.started", "run.completed"]
carrier = "durable"
failure_posture = "gap"
[[telemetry.channels]]
kinds = ["turn.delta", "*.heartbeat"]
carrier = "ephemeral"
failure_posture = "stop"
"#
),
);
let config = KhiveConfig::load(Some(&path)).unwrap().unwrap();
assert_eq!(config.telemetry.channels.len(), 2);
let resolved =
crate::runtime_config_from_khive_config(&config, in_memory_runtime_config());
assert_eq!(resolved.telemetry, config.telemetry);
assert_eq!(resolved.telemetry.stream, "operations");
for kind in ["run.started", "run.completed"] {
let policy = resolved.telemetry.policy_for_kind(kind).unwrap();
assert_eq!(policy.carrier, TelemetryCarrier::Durable);
assert_eq!(policy.failure_posture, TelemetryFailurePosture::Gap);
}
for kind in ["turn.delta", "run.heartbeat", "turn.child.heartbeat"] {
let policy = resolved.telemetry.policy_for_kind(kind).unwrap();
assert_eq!(policy.carrier, TelemetryCarrier::Ephemeral);
assert_eq!(policy.failure_posture, TelemetryFailurePosture::Stop);
}
for kind in [
"unclassified",
"heartbeat",
"run.notheartbeat",
"run.heartbeat.extra",
] {
let policy = resolved.telemetry.policy_for_kind(kind).unwrap();
assert_eq!(policy.carrier, TelemetryCarrier::Durable);
assert_eq!(policy.failure_posture, TelemetryFailurePosture::Stop);
}
}
}
#[test]
fn telemetry_invalid_policy_values_name_the_channel() {
let dir = tempfile::tempdir().unwrap();
for (carrier, posture, field, value) in [
("disk", "stop", "carrier", "disk"),
("Durable", "stop", "carrier", "Durable"),
("durable", "ignore", "failure_posture", "ignore"),
("durable", "Stop", "failure_posture", "Stop"),
] {
let path = write_toml(
&dir,
&format!(
r#"[[telemetry.channels]]
kinds = ["first"]
carrier = "ephemeral"
failure_posture = "gap"
[[telemetry.channels]]
kinds = ["second"]
carrier = "{carrier}"
failure_posture = "{posture}"
"#
),
);
let error = KhiveConfig::load(Some(&path)).expect_err("invalid policy must refuse");
let message = error.to_string();
for expected in ["telemetry.channels[1]", field, value] {
assert!(message.contains(expected), "{message}");
}
}
let path = write_toml(&dir, "[telemetry]\ndefault_carrier = \"disk\"\n");
let error = KhiveConfig::load(Some(&path)).expect_err("unknown fallback must refuse");
assert!(
error.to_string().contains("telemetry.default_carrier"),
"{error}"
);
}
#[test]
fn telemetry_overlapping_channels_name_both_entries() {
let dir = tempfile::tempdir().unwrap();
for (first, second) in [
("run.started", "run.started"),
("run.heartbeat", "*.heartbeat"),
("*.heartbeat", "run.heartbeat"),
("*.heartbeat", "*.heartbeat"),
("*.heartbeat", "*.child.heartbeat"),
("*.child.heartbeat", "*.heartbeat"),
] {
let path = write_toml(
&dir,
&format!(
r#"[[telemetry.channels]]
kinds = ["{first}"]
carrier = "ephemeral"
failure_posture = "gap"
[[telemetry.channels]]
kinds = ["{second}"]
carrier = "durable"
failure_posture = "stop"
"#
),
);
let error = KhiveConfig::load(Some(&path)).expect_err("overlap must refuse");
let message = error.to_string();
for expected in [
"telemetry.channels[1]",
"telemetry.channels[0]",
first,
second,
] {
assert!(message.contains(expected), "{message}");
}
}
}
#[test]
fn telemetry_empty_and_invalid_kind_patterns_name_the_channel() {
let dir = tempfile::tempdir().unwrap();
for kinds in [
"[]",
"[\"\"]",
"[\" \"]",
"[\"two names\"]",
"[\"*\"]",
"[\"run.*\"]",
"[\"*.\"]",
"[\"**.heartbeat\"]",
"[\"*.heart*beat\"]",
] {
let path = write_toml(
&dir,
&format!(
r#"[[telemetry.channels]]
kinds = ["first"]
carrier = "ephemeral"
failure_posture = "gap"
[[telemetry.channels]]
kinds = {kinds}
carrier = "durable"
failure_posture = "stop"
"#
),
);
let error = KhiveConfig::load(Some(&path)).expect_err("invalid kinds must refuse");
assert!(
error.to_string().contains("telemetry.channels[1]"),
"{error}"
);
}
}
#[test]
fn telemetry_tables_reject_unknown_keys() {
let dir = tempfile::tempdir().unwrap();
for content in [
"[telemetry]\ndefault_carrrier = \"durable\"\n",
"[telemetry.ring]\ncapacity = 4096\n",
"[[telemetry.channels]]\nkinds = [\"run\"]\ncarrier = \"durable\"\nfailure_posture = \"stop\"\ncarrrier = \"ephemeral\"\n",
] {
let path = write_toml(&dir, content);
let error = KhiveConfig::load(Some(&path)).expect_err("unknown key must refuse");
assert!(error.to_string().contains("unknown field"), "{error}");
}
}
#[test]
fn test_no_git_write_section_is_valid_and_empty() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(&dir, "# no git_write section\n");
let cfg = KhiveConfig::load(Some(&path))
.expect("no error")
.expect("file found");
assert!(cfg.git_write.allowed.is_empty());
}
fn write_git_program_config(dir: &tempfile::TempDir, program: &Path) -> PathBuf {
let program = toml::Value::String(program.to_str().unwrap().to_string());
write_toml(dir, &format!("[git_write]\nprogram = {program}\n"))
}
#[test]
fn git_program_absent_preserves_path_default() {
let dir = tempfile::tempdir().unwrap();
for content in ["# no git_write section\n", "[git_write]\n"] {
let path = write_toml(&dir, content);
let cfg = KhiveConfig::load(Some(&path)).unwrap().unwrap();
assert!(cfg.git_write.program.is_none());
assert_eq!(cfg.git_write.git_program(), Path::new("git"));
}
assert!(GitWriteSectionConfig::default().program.is_none());
assert_eq!(
GitWriteSectionConfig::default().git_program(),
Path::new("git")
);
}
#[cfg(any(unix, windows))]
#[test]
fn git_program_absolute_executable_loads() {
let dir = tempfile::tempdir().unwrap();
let program = std::env::current_exe().unwrap();
let path = write_git_program_config(&dir, &program);
let cfg = KhiveConfig::load(Some(&path)).unwrap().unwrap();
assert_eq!(cfg.git_write.program.as_deref(), Some(program.as_path()));
assert_eq!(cfg.git_write.git_program(), program);
}
#[test]
fn git_program_relative_path_is_rejected_at_load() {
let dir = tempfile::tempdir().unwrap();
for program in ["git", "relative/git"] {
let path = write_git_program_config(&dir, Path::new(program));
let error = KhiveConfig::load(Some(&path)).expect_err("relative program must fail");
assert!(
matches!(config_error_root(&error), ConfigError::InvalidGitWriteConfig { key, reason }
if key == "git_write.program" && reason == "must be absolute"),
"unexpected error: {error}"
);
assert!(error.to_string().contains("git_write.program"));
}
}
#[test]
fn git_program_missing_file_is_rejected_at_load() {
let dir = tempfile::tempdir().unwrap();
let path = write_git_program_config(&dir, &dir.path().join("missing-git"));
let error = KhiveConfig::load(Some(&path)).expect_err("missing program must fail");
assert!(
matches!(config_error_root(&error), ConfigError::InvalidGitWriteConfig { key, reason }
if key == "git_write.program" && reason == "does not exist"),
"unexpected error: {error}"
);
assert!(error.to_string().contains("git_write.program"));
}
#[test]
fn git_program_nonexecutable_file_is_rejected_at_load() {
let dir = tempfile::tempdir().unwrap();
let program = dir.path().join("git.txt");
std::fs::write(&program, "not executable\n").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&program, std::fs::Permissions::from_mode(0o600)).unwrap();
}
let path = write_git_program_config(&dir, &program);
let error = KhiveConfig::load(Some(&path)).expect_err("nonexecutable program must fail");
assert!(
matches!(config_error_root(&error), ConfigError::InvalidGitWriteConfig { key, reason }
if key == "git_write.program" && reason == "is not executable"),
"unexpected error: {error}"
);
assert!(error.to_string().contains("git_write.program"));
}
#[test]
fn git_program_directory_is_rejected_at_load() {
let dir = tempfile::tempdir().unwrap();
let program = dir.path().join("git.exe");
std::fs::create_dir(&program).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&program, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let path = write_git_program_config(&dir, &program);
let error = KhiveConfig::load(Some(&path)).expect_err("directory program must fail");
assert!(
matches!(config_error_root(&error), ConfigError::InvalidGitWriteConfig { key, reason }
if key == "git_write.program" && reason == "is not executable"),
"unexpected error: {error}"
);
assert!(error.to_string().contains("git_write.program"));
}
#[test]
fn test_git_write_entry_parses() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[git_write.allowed]]
repo = "/abs/path/repo"
branches = ["feat/*", "fix/*"]
"#,
);
let cfg = KhiveConfig::load(Some(&path))
.expect("no error")
.expect("file found");
assert_eq!(cfg.git_write.allowed.len(), 1);
assert_eq!(cfg.git_write.allowed[0].repo, "/abs/path/repo");
assert_eq!(
cfg.git_write.allowed[0].branches,
vec!["feat/*".to_string(), "fix/*".to_string()]
);
}
#[test]
fn test_git_write_relative_repo_rejected() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[git_write.allowed]]
repo = "relative/path"
branches = ["main"]
"#,
);
let err = KhiveConfig::load(Some(&path)).expect_err("relative repo must be rejected");
assert!(
matches!(config_error_root(&err), ConfigError::InvalidGitWriteEntry { ref repo, .. } if repo == "relative/path"),
"expected InvalidGitWriteEntry, got {err:?}"
);
}
#[test]
fn test_git_write_multi_star_branch_pattern_rejected() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[git_write.allowed]]
repo = "/abs/path"
branches = ["**"]
"#,
);
let err = KhiveConfig::load(Some(&path)).expect_err("** must be rejected");
assert!(
matches!(config_error_root(&err), ConfigError::InvalidGitWriteEntry { ref repo, .. } if repo == "/abs/path"),
"expected InvalidGitWriteEntry, got {err:?}"
);
let dir2 = tempfile::tempdir().unwrap();
let path2 = write_toml(
&dir2,
r#"
[[git_write.allowed]]
repo = "/abs/path"
branches = ["rel-*-*-final"]
"#,
);
let err2 = KhiveConfig::load(Some(&path2)).expect_err("rel-*-*-final must be rejected");
assert!(
matches!(
config_error_root(&err2),
ConfigError::InvalidGitWriteEntry { .. }
),
"expected InvalidGitWriteEntry, got {err2:?}"
);
}
#[test]
fn test_git_write_single_star_branch_pattern_accepted() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[git_write.allowed]]
repo = "/abs/path"
branches = ["a*b", "main"]
"#,
);
let cfg = KhiveConfig::load(Some(&path))
.expect("no error")
.expect("file found");
assert_eq!(cfg.git_write.allowed[0].branches, vec!["a*b", "main"]);
}
#[test]
fn test_git_write_empty_branches_rejected() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[[git_write.allowed]]
repo = "/abs/path"
branches = []
"#,
);
let err = KhiveConfig::load(Some(&path)).expect_err("empty branches must be rejected");
assert!(
matches!(config_error_root(&err), ConfigError::InvalidGitWriteEntry { ref repo, .. } if repo == "/abs/path"),
"expected InvalidGitWriteEntry, got {err:?}"
);
}
#[test]
fn git_actor_mapping_and_resolver_defaults_parse_without_resolution() {
let cfg: KhiveConfig = toml::from_str(
r#"
[git_write.actors."lambda:example"]
name = "Example"
email = "example@example.invalid"
credential_ref = "example-reference"
platform_identity = "example-login"
"#,
)
.unwrap();
if cfg!(unix) {
cfg.validate().unwrap();
} else {
assert!(cfg.validate().is_err());
}
let identity = &cfg.git_write.actors["lambda:example"];
assert_eq!(identity.name, "Example");
assert_eq!(identity.credential_ref, "example-reference");
assert_eq!(
cfg.git_write.credential_resolver,
GitWriteSectionConfig::default().credential_resolver
);
}
#[test]
fn git_resolver_accepts_only_absolute_argv_with_ref_template() {
for argv in [
vec![],
vec!["relative-resolver", "{ref}"],
vec!["/bin/sh", "-c", "{ref}"],
vec!["/usr/bin/env", "sh", "{ref}"],
vec!["/absolute/resolver", "{token}"],
vec!["/absolute/resolver", "--service={ref}"],
vec!["/absolute/resolver"],
vec!["/absolute/resolver", "{ref}", "bad\0arg"],
] {
let config = GitWriteSectionConfig {
credential_resolver: argv.into_iter().map(str::to_string).collect(),
..Default::default()
};
assert!(matches!(
config.validate_dev_loop(),
Err(ConfigError::InvalidGitWriteConfig { key, .. }) if key == "credential_resolver"
));
}
let config = GitWriteSectionConfig {
credential_resolver: vec![
std::env::temp_dir()
.join("not-installed-yet/resolver")
.to_string_lossy()
.into_owned(),
"--reference".to_string(),
"{ref}".to_string(),
],
..Default::default()
};
config.validate_dev_loop().unwrap();
}
#[test]
fn git_actor_mapping_rejects_invalid_identity_and_unknown_fields() {
let actor = GitWriteActorConfig {
name: "Example".to_string(),
email: "example@example.invalid".to_string(),
credential_ref: "example-reference".to_string(),
platform_identity: "example-login".to_string(),
};
for field in ["name", "email", "credential_ref", "platform_identity"] {
let mut invalid = actor.clone();
match field {
"name" => invalid.name.clear(),
"email" => invalid.email = "bad\nemail".to_string(),
"credential_ref" => invalid.credential_ref.clear(),
"platform_identity" => invalid.platform_identity.clear(),
_ => unreachable!(),
}
let config = GitWriteSectionConfig {
actors: BTreeMap::from([("example".to_string(), invalid)]),
..Default::default()
};
assert!(config.validate_dev_loop().is_err());
}
assert!(toml::from_str::<GitWriteActorConfig>(
r#"name = "Example"
email = "example@example.invalid"
credential_ref = "reference"
platform_identity = "login"
credential = "not-an-accepted-field""#
)
.is_err());
}
#[test]
fn git_repository_merge_refusals_accept_only_the_two_named_entries() {
let row = |refusals: &[&str]| GitWriteSectionConfig {
repositories: BTreeMap::from([(
"/repo".to_string(),
GitWriteRepositoryConfig {
remote: "https://github.com/example/repo".to_string(),
slug: "example/repo".to_string(),
visibility: "private".to_string(),
merge_refusals: refusals.iter().map(|entry| entry.to_string()).collect(),
},
)]),
..Default::default()
};
for refusals in [
&[][..],
&["opener"][..],
&["last_pusher"][..],
&["opener", "last_pusher"][..],
] {
row(refusals).validate_dev_loop().unwrap();
}
for refusals in [
&["author"][..],
&["Opener"][..],
&["opener", "opener"][..],
&["last_pusher", "opener", "last_pusher"][..],
] {
assert!(matches!(
row(refusals).validate_dev_loop(),
Err(ConfigError::InvalidGitWriteConfig { key, .. })
if key == "repositories./repo.merge_refusals"
));
}
let parsed: GitWriteRepositoryConfig = toml::from_str(
r#"remote = "https://github.com/example/repo"
slug = "example/repo"
visibility = "private""#,
)
.unwrap();
assert!(parsed.merge_refusals.is_empty());
assert!(toml::from_str::<GitWriteRepositoryConfig>(
r#"remote = "https://github.com/example/repo"
slug = "example/repo"
visibility = "private"
merge_refusal = ["opener"]"#
)
.is_err());
}
#[test]
fn git_contract_faults_are_feature_gated_before_empty_engines_return() {
let cfg = KhiveConfig {
git_write: GitWriteSectionConfig {
contract_faults: true,
..Default::default()
},
..Default::default()
};
if cfg!(feature = "contract-faults") {
cfg.validate().unwrap();
} else {
let error = cfg.validate().unwrap_err();
assert!(matches!(error, ConfigError::InvalidGitWriteConfig { .. }));
assert!(error.to_string().contains("contract-faults"));
}
}
#[test]
fn git_unmapped_legacy_default_remains_valid_on_every_platform() {
GitWriteSectionConfig::default()
.validate_dev_loop()
.unwrap();
let config = GitWriteSectionConfig {
credential_resolver: vec!["relative-resolver".to_string(), "{ref}".to_string()],
..Default::default()
};
assert!(config.validate_dev_loop().is_err());
}
#[test]
fn git_fault_selectors_require_opt_in() {
let config = GitWriteSectionConfig {
fault: Some("git.push:reply-lost-after-effect".to_string()),
..Default::default()
};
assert!(matches!(
config.validate_dev_loop(),
Err(ConfigError::InvalidGitWriteConfig { key, .. }) if key == "fault"
));
}
#[test]
fn test_no_display_section_defaults_to_none() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(&dir, "# no display section\n");
let cfg = KhiveConfig::load(Some(&path))
.expect("no error")
.expect("file found");
assert!(cfg.display.timezone.is_none());
}
#[test]
fn test_display_timezone_valid_iana_name_parses() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[display]
timezone = "America/New_York"
"#,
);
let cfg = KhiveConfig::load(Some(&path))
.expect("no error")
.expect("file found");
assert_eq!(cfg.display.timezone.as_deref(), Some("America/New_York"));
}
#[test]
fn test_display_timezone_unrecognized_name_rejected() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[display]
timezone = "Mars/Olympus_Mons"
"#,
);
let err = KhiveConfig::load(Some(&path))
.expect_err("an unrecognized IANA zone name must fail at load, not silently fall back");
assert!(
matches!(config_error_root(&err), ConfigError::InvalidDisplayTimezone { ref timezone } if timezone == "Mars/Olympus_Mons"),
"expected InvalidDisplayTimezone, got {err:?}"
);
}
#[test]
fn test_display_timezone_empty_string_rejected() {
let dir = tempfile::tempdir().unwrap();
let path = write_toml(
&dir,
r#"
[display]
timezone = ""
"#,
);
let err =
KhiveConfig::load(Some(&path)).expect_err("an empty timezone string must be rejected");
assert!(
matches!(
config_error_root(&err),
ConfigError::InvalidDisplayTimezone { .. }
),
"expected InvalidDisplayTimezone, got {err:?}"
);
}
#[test]
fn wal_ceiling_alias_conflict_escapes_control_characters_in_the_path() {
let error = ConfigError::WalCeilingAliasConflict {
first_backend: "main".to_string(),
second_backend: "alias".to_string(),
path: PathBuf::from("/data/line\nforged entry\x1b[31m/archive.db"),
first_bytes: 0,
second_bytes: 8192,
};
let text = error.to_string();
assert!(
!text.chars().any(|c| c == '\n' || c == '\x1b'),
"a configured path must not put raw control characters in the error text; got {text:?}"
);
assert!(
text.contains("line\\u{000a}forged entry\\u{001b}[31m/archive.db"),
"control characters must be escaped in place; got {text:?}"
);
assert!(text.contains("resolve different WAL ceilings (0 and 8192 bytes)"));
}
include!("engine_config_backend_batch_tests.rs");
include!("engine_config_storage_tests.rs");
}