use std::collections::HashSet;
use std::fmt;
use std::path::{Path, PathBuf};
use oxibrain_core::extraction::ExtractMechanism;
use oxibrain_ports::LlmCapabilities;
use serde::{Deserialize, Serialize};
pub const SCHEMA_VERSION: u32 = 1;
#[allow(dead_code)]
pub const ALLOWED_ROLES: &[&str] = &[
"memory.extract",
"memory.consolidate",
"coding.primary",
"assistant.general",
];
pub const SECRET_FIELD_NAMES: &[&str] = &[
"api_key",
"apikey",
"api-token",
"bearer",
"access_token",
"refresh_token",
"secret",
"password",
"private_key",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProfileRole {
#[serde(rename = "memory.extract")]
MemoryExtract,
#[serde(rename = "memory.consolidate")]
MemoryConsolidate,
#[serde(rename = "coding.primary")]
CodingPrimary,
#[serde(rename = "assistant.general")]
AssistantGeneral,
}
impl ProfileRole {
#[allow(dead_code)]
pub fn as_str(self) -> &'static str {
match self {
ProfileRole::MemoryExtract => "memory.extract",
ProfileRole::MemoryConsolidate => "memory.consolidate",
ProfileRole::CodingPrimary => "coding.primary",
ProfileRole::AssistantGeneral => "assistant.general",
}
}
pub fn parse(s: &str) -> Option<Self> {
Some(match s {
"memory.extract" => ProfileRole::MemoryExtract,
"memory.consolidate" => ProfileRole::MemoryConsolidate,
"coding.primary" => ProfileRole::CodingPrimary,
"assistant.general" => ProfileRole::AssistantGeneral,
_ => return None,
})
}
}
impl fmt::Display for ProfileRole {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SecretLocator {
pub service: String,
pub account: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct DeclaredCapabilities {
pub grammar: bool,
pub structured_output: bool,
pub tool_call: bool,
pub json_schema: bool,
}
impl DeclaredCapabilities {
pub fn satisfies(&self, mechanism: ExtractMechanism) -> bool {
match mechanism {
ExtractMechanism::Grammar => self.grammar,
ExtractMechanism::JsonSchema => self.json_schema || self.structured_output,
ExtractMechanism::ToolCall => self.tool_call,
ExtractMechanism::JsonMode => true,
}
}
#[allow(dead_code, clippy::wrong_self_convention)]
pub fn as_llm_capabilities(self) -> LlmCapabilities {
LlmCapabilities {
grammar: self.grammar,
structured_output: self.structured_output,
tool_call: self.tool_call,
json_schema: self.json_schema,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProviderProfile {
pub id: String,
pub provider: String,
pub model: String,
pub roles: Vec<ProfileRole>,
pub credential: SecretLocator,
#[serde(default)]
pub capabilities: DeclaredCapabilities,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FoundationProfiles {
pub schema_version: u32,
pub profiles: Vec<ProviderProfile>,
}
#[derive(Debug, Clone)]
pub struct ResolvedProfiles {
pub profiles: Vec<ProviderProfile>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FoundationError {
UnsupportedSchemaVersion(u32),
InvalidShape(String),
SecretFieldPresent(String),
DuplicateProfileId(String),
#[allow(dead_code)]
UnknownRole(String),
DuplicateRole(ProfileRole),
EmptyField(&'static str),
EmptyRoles,
IoError(String),
SecretUnavailable {
service: String,
account: String,
reason: String,
},
#[allow(dead_code)]
CapabilityUnsatisfied {
profile_id: String,
mechanism: ExtractMechanism,
},
#[allow(dead_code)]
RoleDenied {
profile_id: String,
requested: ProfileRole,
},
}
impl fmt::Display for FoundationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FoundationError::UnsupportedSchemaVersion(v) => {
write!(
f,
"profiles.json schema_version={v} is not supported (expected 1)"
)
}
FoundationError::InvalidShape(detail) => {
write!(f, "profiles.json shape invalid: {detail}")
}
FoundationError::SecretFieldPresent(field) => write!(
f,
"profiles.json rejected: carries secret-shaped field `{field}` (§2.5)"
),
FoundationError::DuplicateProfileId(id) => {
write!(f, "profiles.json rejected: duplicate profile id `{id}`")
}
FoundationError::UnknownRole(role) => write!(
f,
"profiles.json rejected: role `{role}` is not one of memory.extract / memory.consolidate / coding.primary / assistant.general"
),
FoundationError::DuplicateRole(role) => write!(
f,
"profiles.json rejected: role `{role}` appears twice in the same profile"
),
FoundationError::EmptyField(field) => write!(
f,
"profiles.json rejected: field `{field}` is the empty string"
),
FoundationError::EmptyRoles => {
write!(f, "profiles.json rejected: a profile lists no roles")
}
FoundationError::IoError(detail) => write!(f, "profiles.json I/O error: {detail}"),
FoundationError::SecretUnavailable {
service,
account,
reason,
} => write!(
f,
"Foundation profile secret unavailable (Keychain service=`{service}` account=`{account}`): {reason}"
),
FoundationError::CapabilityUnsatisfied {
profile_id,
mechanism,
} => write!(
f,
"Foundation profile `{profile_id}` rejected: declared capabilities do not satisfy extraction mechanism {mechanism:?}"
),
FoundationError::RoleDenied {
profile_id,
requested,
} => write!(
f,
"Foundation profile `{profile_id}` rejected: does not declare role `{requested}`"
),
}
}
}
impl std::error::Error for FoundationError {}
pub trait SecretResolver: Send + Sync {
fn resolve(&self, locator: &SecretLocator) -> Result<String, FoundationError>;
}
#[derive(Debug, Default, Clone)]
pub struct InMemorySecretResolver {
entries: std::collections::HashMap<(String, String), String>,
}
impl InMemorySecretResolver {
#[allow(dead_code)]
pub fn new() -> Self {
Self::default()
}
#[allow(dead_code)]
pub fn with_secret(
mut self,
service: impl Into<String>,
account: impl Into<String>,
secret: impl Into<String>,
) -> Self {
self.entries
.insert((service.into(), account.into()), secret.into());
self
}
}
impl SecretResolver for InMemorySecretResolver {
fn resolve(&self, locator: &SecretLocator) -> Result<String, FoundationError> {
self.entries
.get(&(locator.service.clone(), locator.account.clone()))
.cloned()
.ok_or_else(|| FoundationError::SecretUnavailable {
service: locator.service.clone(),
account: locator.account.clone(),
reason: "no entry in InMemorySecretResolver (test default)".into(),
})
}
}
#[cfg(feature = "os-keychain")]
pub struct OsKeychainResolver {
service_prefix: String,
}
#[cfg(feature = "os-keychain")]
impl OsKeychainResolver {
pub fn new() -> Self {
Self {
service_prefix: "oxibrain/foundation/v1/".to_string(),
}
}
#[allow(dead_code)]
pub fn with_service_prefix(prefix: impl Into<String>) -> Self {
Self {
service_prefix: prefix.into(),
}
}
}
#[cfg(feature = "os-keychain")]
impl Default for OsKeychainResolver {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "os-keychain")]
impl SecretResolver for OsKeychainResolver {
fn resolve(&self, locator: &SecretLocator) -> Result<String, FoundationError> {
use std::collections::BTreeMap;
thread_local! {
static CACHE: std::cell::RefCell<BTreeMap<(String, String), Result<String, String>>> =
const { std::cell::RefCell::new(BTreeMap::new()) };
}
let service = format!("{}{}", self.service_prefix, locator.service);
let key = (service.clone(), locator.account.clone());
CACHE.with(|cache| {
if let Some(cached) = cache.borrow().get(&key) {
return cached
.clone()
.map_err(|reason| FoundationError::SecretUnavailable {
service: locator.service.clone(),
account: locator.account.clone(),
reason,
});
}
let entry = keyring::Entry::new(&service, &locator.account);
let outcome = match entry.and_then(|e| e.get_password()) {
Ok(secret) => Ok(secret),
Err(e) => Err(e.to_string()),
};
cache.borrow_mut().insert(key, outcome.clone());
outcome.map_err(|reason| FoundationError::SecretUnavailable {
service: locator.service.clone(),
account: locator.account.clone(),
reason,
})
})
}
}
pub fn default_secret_resolver() -> Box<dyn SecretResolver> {
#[cfg(feature = "os-keychain")]
{
Box::new(OsKeychainResolver::new())
}
#[cfg(not(feature = "os-keychain"))]
{
Box::new(InMemorySecretResolver::new())
}
}
pub fn foundation_home() -> PathBuf {
if let Some(home) = std::env::var_os("OXI_FOUNDATION_HOME") {
PathBuf::from(home)
} else if let Some(home) = std::env::var_os("HOME") {
PathBuf::from(home)
.join(".oxi")
.join("foundation")
.join("v1")
} else {
PathBuf::from(".oxi").join("foundation").join("v1")
}
}
fn profiles_path(home: &Path) -> PathBuf {
home.join("profiles.json")
}
pub fn load_profiles(home: &Path) -> Result<Option<ResolvedProfiles>, FoundationError> {
let path = profiles_path(home);
let bytes = match std::fs::read(&path) {
Ok(b) => b,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => {
return Err(FoundationError::IoError(format!("{}: {e}", path.display())));
}
};
let raw: serde_json::Value = serde_json::from_slice(&bytes).map_err(|e| {
FoundationError::InvalidShape(format!("profiles.json is not valid JSON: {e}"))
})?;
let obj = raw
.as_object()
.ok_or_else(|| FoundationError::InvalidShape("root is not a JSON object".into()))?;
for profile_value in obj
.get("profiles")
.and_then(|p| p.as_array())
.ok_or_else(|| FoundationError::InvalidShape("`profiles` is not an array".into()))?
{
let profile_obj = profile_value.as_object().ok_or_else(|| {
FoundationError::InvalidShape("a profile entry is not a JSON object".into())
})?;
for key in profile_obj.keys() {
if SECRET_FIELD_NAMES.iter().any(|s| s == key) {
return Err(FoundationError::SecretFieldPresent(key.clone()));
}
}
}
let parsed: FoundationProfiles = serde_json::from_value(raw)
.map_err(|e| FoundationError::InvalidShape(format!("profiles.json: {e}")))?;
if parsed.schema_version != SCHEMA_VERSION {
return Err(FoundationError::UnsupportedSchemaVersion(
parsed.schema_version,
));
}
let mut seen_ids: HashSet<String> = HashSet::new();
for profile in &parsed.profiles {
if profile.id.is_empty() {
return Err(FoundationError::EmptyField("id"));
}
if profile.provider.is_empty() {
return Err(FoundationError::EmptyField("provider"));
}
if profile.model.is_empty() {
return Err(FoundationError::EmptyField("model"));
}
if profile.credential.service.is_empty() {
return Err(FoundationError::EmptyField("credential.service"));
}
if profile.credential.account.is_empty() {
return Err(FoundationError::EmptyField("credential.account"));
}
if !seen_ids.insert(profile.id.clone()) {
return Err(FoundationError::DuplicateProfileId(profile.id.clone()));
}
if profile.roles.is_empty() {
return Err(FoundationError::EmptyRoles);
}
let mut seen_roles: HashSet<ProfileRole> = HashSet::new();
for role in &profile.roles {
if !seen_roles.insert(*role) {
return Err(FoundationError::DuplicateRole(*role));
}
}
}
Ok(Some(ResolvedProfiles {
profiles: parsed.profiles,
}))
}
impl ResolvedProfiles {
#[allow(dead_code)]
pub fn pick_for_role(
&self,
role: ProfileRole,
mechanism: ExtractMechanism,
) -> Result<&ProviderProfile, FoundationError> {
for profile in &self.profiles {
if !profile.roles.contains(&role) {
continue;
}
if !profile.capabilities.clone().satisfies(mechanism) {
return Err(FoundationError::CapabilityUnsatisfied {
profile_id: profile.id.clone(),
mechanism,
});
}
return Ok(profile);
}
Err(FoundationError::RoleDenied {
profile_id: self
.profiles
.first()
.map(|p| p.id.clone())
.unwrap_or_default(),
requested: role,
})
}
pub fn iter(&self) -> std::slice::Iter<'_, ProviderProfile> {
self.profiles.iter()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProviderKind {
Anthropic,
OpenAi,
}
impl ProviderKind {
pub fn parse(s: &str) -> Option<Self> {
Some(match s {
"anthropic" | "claude" => ProviderKind::Anthropic,
"openai" | "gpt" => ProviderKind::OpenAi,
_ => return None,
})
}
#[allow(dead_code)]
pub fn as_str(self) -> &'static str {
match self {
ProviderKind::Anthropic => "anthropic",
ProviderKind::OpenAi => "openai",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn write_profiles(dir: &Path, body: &str) {
std::fs::create_dir_all(dir).unwrap();
std::fs::write(dir.join("profiles.json"), body).unwrap();
}
#[test]
fn missing_file_is_not_an_error() {
let dir = tempfile::tempdir().unwrap();
let got = load_profiles(dir.path()).unwrap();
assert!(got.is_none());
}
#[test]
fn rejects_secret_shaped_fields() {
let dir = tempfile::tempdir().unwrap();
write_profiles(
dir.path(),
r#"{
"schema_version": 1,
"profiles": [
{
"id": "leaky",
"provider": "anthropic",
"model": "claude-sonnet-4-5",
"roles": ["memory.extract"],
"credential": {"service": "oxibrain", "account": "a7"},
"api_key": "sk-test"
}
]
}"#,
);
let err = load_profiles(dir.path()).unwrap_err();
assert!(matches!(&err, FoundationError::SecretFieldPresent(f) if f == "api_key"));
}
#[test]
fn rejects_each_secret_field_by_name() {
for field in SECRET_FIELD_NAMES {
let dir = tempfile::tempdir().unwrap();
let body = format!(
r#"{{"schema_version":1,"profiles":[{{"id":"p","provider":"anthropic","model":"m","roles":["memory.extract"],"credential":{{"service":"s","account":"a"}},"{field}":"x"}}]}}"#
);
write_profiles(dir.path(), &body);
let err = load_profiles(dir.path()).unwrap_err();
assert!(
matches!(&err, FoundationError::SecretFieldPresent(f) if f == field),
"expected SecretFieldPresent({field}), got {err:?}"
);
}
}
#[test]
fn rejects_unsupported_schema_version() {
let dir = tempfile::tempdir().unwrap();
write_profiles(dir.path(), r#"{"schema_version":2,"profiles":[]}"#);
assert!(matches!(
load_profiles(dir.path()),
Err(FoundationError::UnsupportedSchemaVersion(2))
));
}
#[test]
fn empty_profiles_array_is_valid() {
let dir = tempfile::tempdir().unwrap();
write_profiles(dir.path(), r#"{"schema_version":1,"profiles":[]}"#);
let got = load_profiles(dir.path()).unwrap().unwrap();
assert!(got.profiles.is_empty());
}
#[test]
fn rejects_duplicate_profile_id() {
let dir = tempfile::tempdir().unwrap();
let body = r#"{
"schema_version": 1,
"profiles": [
{"id":"same","provider":"anthropic","model":"m","roles":["memory.extract"],"credential":{"service":"s","account":"a"}},
{"id":"same","provider":"openai","model":"m","roles":["memory.extract"],"credential":{"service":"s","account":"a"}}
]
}"#;
write_profiles(dir.path(), body);
assert!(matches!(
&load_profiles(dir.path()),
Err(FoundationError::DuplicateProfileId(id)) if id == "same"
));
}
#[test]
fn rejects_unknown_role() {
let dir = tempfile::tempdir().unwrap();
let body = r#"{
"schema_version": 1,
"profiles": [
{"id":"p","provider":"anthropic","model":"m","roles":["memory.unknown"],"credential":{"service":"s","account":"a"}}
]
}"#;
write_profiles(dir.path(), body);
let err = load_profiles(dir.path()).unwrap_err();
assert!(matches!(err, FoundationError::InvalidShape(_)));
}
#[test]
fn rejects_empty_roles() {
let dir = tempfile::tempdir().unwrap();
let body = r#"{
"schema_version": 1,
"profiles": [
{"id":"p","provider":"anthropic","model":"m","roles":[],"credential":{"service":"s","account":"a"}}
]
}"#;
write_profiles(dir.path(), body);
assert!(matches!(
&load_profiles(dir.path()),
Err(FoundationError::EmptyRoles)
));
}
#[test]
fn rejects_duplicate_role_in_profile() {
let dir = tempfile::tempdir().unwrap();
let body = r#"{
"schema_version": 1,
"profiles": [
{"id":"p","provider":"anthropic","model":"m","roles":["memory.extract","memory.extract"],"credential":{"service":"s","account":"a"}}
]
}"#;
write_profiles(dir.path(), body);
assert!(matches!(
&load_profiles(dir.path()),
Err(FoundationError::DuplicateRole(ProfileRole::MemoryExtract))
));
}
#[test]
fn rejects_empty_provider_or_model() {
let dir = tempfile::tempdir().unwrap();
let body = r#"{
"schema_version": 1,
"profiles": [
{"id":"p","provider":"","model":"m","roles":["memory.extract"],"credential":{"service":"s","account":"a"}}
]
}"#;
write_profiles(dir.path(), body);
assert!(matches!(
&load_profiles(dir.path()),
Err(FoundationError::EmptyField("provider"))
));
}
#[test]
fn rejects_empty_credential_locator() {
let dir = tempfile::tempdir().unwrap();
let body = r#"{
"schema_version": 1,
"profiles": [
{"id":"p","provider":"anthropic","model":"m","roles":["memory.extract"],"credential":{"service":"","account":"a"}}
]
}"#;
write_profiles(dir.path(), body);
assert!(matches!(
&load_profiles(dir.path()),
Err(FoundationError::EmptyField("credential.service"))
));
}
#[test]
fn accepts_well_formed_canonical_profile() {
let dir = tempfile::tempdir().unwrap();
let body = r#"{
"schema_version": 1,
"profiles": [
{
"id": "work-summariser",
"provider": "anthropic",
"model": "claude-sonnet-4-5",
"roles": ["memory.consolidate", "assistant.general"],
"credential": {"service": "oxibrain", "account": "work"}
}
]
}"#;
write_profiles(dir.path(), body);
let got = load_profiles(dir.path()).unwrap().unwrap();
assert_eq!(got.profiles.len(), 1);
assert_eq!(got.profiles[0].id, "work-summariser");
assert_eq!(got.profiles[0].provider, "anthropic");
assert_eq!(
got.profiles[0].roles,
vec![
ProfileRole::MemoryConsolidate,
ProfileRole::AssistantGeneral
]
);
}
#[test]
fn pick_for_role_skips_non_members() {
let dir = tempfile::tempdir().unwrap();
let body = r#"{
"schema_version": 1,
"profiles": [
{"id":"a","provider":"anthropic","model":"m","roles":["coding.primary"],"credential":{"service":"s","account":"a"}},
{"id":"b","provider":"openai","model":"m","roles":["memory.extract"],"credential":{"service":"s","account":"b"},"capabilities":{"grammar":false,"structured_output":true,"tool_call":true,"json_schema":true}}
]
}"#;
write_profiles(dir.path(), body);
let got = load_profiles(dir.path()).unwrap().unwrap();
let pick = got
.pick_for_role(ProfileRole::MemoryExtract, ExtractMechanism::JsonSchema)
.unwrap();
assert_eq!(pick.id, "b");
}
#[test]
fn pick_for_role_rejects_when_capabilities_unsatisfy() {
let dir = tempfile::tempdir().unwrap();
let body = r#"{
"schema_version": 1,
"profiles": [
{
"id":"constrained",
"provider":"anthropic",
"model":"m",
"roles":["memory.extract"],
"credential":{"service":"s","account":"a"},
"capabilities":{"grammar":true,"structured_output":false,"tool_call":false,"json_schema":false}
}
]
}"#;
write_profiles(dir.path(), body);
let got = load_profiles(dir.path()).unwrap().unwrap();
let err = got
.pick_for_role(ProfileRole::MemoryExtract, ExtractMechanism::JsonSchema)
.unwrap_err();
assert!(
matches!(&err, FoundationError::CapabilityUnsatisfied { profile_id, .. } if profile_id == "constrained")
);
}
#[test]
fn pick_for_role_role_denied_when_no_match() {
let dir = tempfile::tempdir().unwrap();
let body = r#"{
"schema_version": 1,
"profiles": [
{"id":"a","provider":"anthropic","model":"m","roles":["coding.primary"],"credential":{"service":"s","account":"a"}}
]
}"#;
write_profiles(dir.path(), body);
let got = load_profiles(dir.path()).unwrap().unwrap();
let err = got
.pick_for_role(ProfileRole::MemoryExtract, ExtractMechanism::JsonSchema)
.unwrap_err();
assert!(matches!(
&err,
FoundationError::RoleDenied {
requested: ProfileRole::MemoryExtract,
..
}
));
}
#[test]
fn in_memory_resolver_hits_and_misses() {
let resolver =
InMemorySecretResolver::new().with_secret("oxibrain", "work", "secret-value");
let hit = resolver
.resolve(&SecretLocator {
service: "oxibrain".into(),
account: "work".into(),
})
.unwrap();
assert_eq!(hit, "secret-value");
let miss = resolver.resolve(&SecretLocator {
service: "oxibrain".into(),
account: "missing".into(),
});
assert!(matches!(
miss,
Err(FoundationError::SecretUnavailable { .. })
));
}
#[test]
fn foundation_home_uses_env_when_set() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let saved = std::env::var_os("OXI_FOUNDATION_HOME");
unsafe {
std::env::set_var("OXI_FOUNDATION_HOME", "/tmp/foundation-test-home");
}
let got = foundation_home();
unsafe {
match saved {
Some(v) => std::env::set_var("OXI_FOUNDATION_HOME", v),
None => std::env::remove_var("OXI_FOUNDATION_HOME"),
}
}
assert_eq!(got, PathBuf::from("/tmp/foundation-test-home"));
}
#[test]
fn declared_capabilities_satisfy() {
let caps = DeclaredCapabilities {
tool_call: true,
..DeclaredCapabilities::default()
};
assert!(caps.clone().satisfies(ExtractMechanism::ToolCall));
assert!(!caps.satisfies(ExtractMechanism::JsonSchema));
assert!(!caps.satisfies(ExtractMechanism::Grammar));
}
#[test]
fn openai_profile_with_only_json_schema_passes_capability_check() {
let dir = tempfile::tempdir().unwrap();
let body = r#"{
"schema_version": 1,
"profiles": [
{
"id": "openai-json",
"provider": "openai",
"model": "gpt-4o",
"roles": ["memory.extract"],
"credential": {"service": "oxibrain", "account": "openai"},
"capabilities": {"grammar": false, "structured_output": false, "tool_call": false, "json_schema": true}
}
]
}"#;
write_profiles(dir.path(), body);
let got = load_profiles(dir.path())
.unwrap()
.expect("profiles present");
let pick = got
.pick_for_role(ProfileRole::MemoryExtract, ExtractMechanism::JsonSchema)
.unwrap();
assert_eq!(pick.id, "openai-json");
}
#[test]
fn role_round_trip() {
for role in [
ProfileRole::MemoryExtract,
ProfileRole::MemoryConsolidate,
ProfileRole::CodingPrimary,
ProfileRole::AssistantGeneral,
] {
assert_eq!(ProfileRole::parse(role.as_str()), Some(role));
}
assert!(ProfileRole::parse("memory.unknown").is_none());
}
}