1use std::collections::{BTreeMap, BTreeSet};
12use std::path::PathBuf;
13
14use serde::{Deserialize, Serialize};
15use thiserror::Error;
16
17use crate::Config;
18use crate::auth::{AuthConstraints, AuthMetadataDefaults};
19use crate::provider::Provider;
20use crate::provider_matrix::{
21 AnthropicAuthMethod, AnthropicBackendKind, GoogleAuthMethod, GoogleBackendKind,
22 OpenAiAuthMethod, OpenAiBackendKind, SelfHostedAuthMethod, SelfHostedBackendKind,
23};
24
25const AZURE_OPENAI_API_KEY_ENV: &str = "AZURE_OPENAI_API_KEY";
26const AZURE_OPENAI_ENDPOINT_ENV: &str = "AZURE_OPENAI_ENDPOINT";
27const AZURE_OPENAI_IMAGE_GENERATION_DEPLOYMENT_ENV: &str =
28 "AZURE_OPENAI_IMAGE_GENERATION_DEPLOYMENT";
29const AZURE_OPENAI_IMAGE_DEPLOYMENT_ENV: &str = "AZURE_OPENAI_IMAGE_DEPLOYMENT";
30const AZURE_OPENAI_IMAGE_GENERATION_API_VERSION_ENV: &str =
31 "AZURE_OPENAI_IMAGE_GENERATION_API_VERSION";
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34struct EnvDefaultSpec {
35 backend_kind: &'static str,
36 auth_method: &'static str,
37 env_var: &'static str,
38 fallback: Vec<String>,
39 base_url: Option<String>,
40 options: serde_json::Value,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)]
49#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
50pub enum IdentityError {
51 #[error("identity slug is empty")]
52 Empty,
53 #[error(
54 "identity slug contains invalid character {0:?}; must be ASCII alphanumeric or one of '-', '_', '.'"
55 )]
56 InvalidChar(char),
57}
58
59fn is_false(value: &bool) -> bool {
62 !*value
63}
64
65fn validate_slug(raw: &str) -> Result<(), IdentityError> {
66 if raw.is_empty() {
67 return Err(IdentityError::Empty);
68 }
69 for ch in raw.chars() {
70 if !(ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' || ch == '.') {
71 return Err(IdentityError::InvalidChar(ch));
72 }
73 }
74 Ok(())
75}
76
77macro_rules! slug_newtype {
78 ($name:ident, $doc:literal) => {
79 #[doc = $doc]
80 #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
81 #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
82 #[serde(try_from = "String", into = "String")]
83 pub struct $name(String);
84
85 impl $name {
86 pub fn parse(raw: impl Into<String>) -> Result<Self, IdentityError> {
87 let raw = raw.into();
88 validate_slug(&raw)?;
89 Ok(Self(raw))
90 }
91
92 #[allow(dead_code)]
100 pub(crate) fn from_known_valid(raw: &'static str) -> Self {
101 debug_assert!(
102 validate_slug(raw).is_ok(),
103 "from_known_valid called with invalid slug literal: {raw:?}",
104 );
105 Self(raw.to_string())
106 }
107
108 pub fn as_str(&self) -> &str {
109 &self.0
110 }
111 }
112
113 impl TryFrom<String> for $name {
114 type Error = IdentityError;
115 fn try_from(s: String) -> Result<Self, Self::Error> {
116 Self::parse(s)
117 }
118 }
119
120 impl From<$name> for String {
121 fn from(v: $name) -> String {
122 v.0
123 }
124 }
125
126 impl std::fmt::Display for $name {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 f.write_str(&self.0)
129 }
130 }
131 };
132}
133
134slug_newtype!(RealmId, "Opaque slug identifying a realm.");
135slug_newtype!(
136 BindingId,
137 "Opaque slug identifying a binding inside a realm."
138);
139slug_newtype!(
140 ProfileId,
141 "Opaque slug identifying an auth profile override on a connection."
142);
143
144pub const ENV_DEFAULT_REALM_SLUG: &str = "env_default";
149
150pub const GLOBAL_REALM_SLUG: &str = "global";
158
159pub const MAX_REALM_CHAIN_DEPTH: usize = 16;
164
165impl RealmId {
166 #[must_use]
171 pub fn is_env_default(&self) -> bool {
172 self.as_str() == ENV_DEFAULT_REALM_SLUG
173 }
174
175 #[must_use]
179 pub fn is_global(&self) -> bool {
180 self.as_str() == GLOBAL_REALM_SLUG
181 }
182
183 #[must_use]
186 pub fn global() -> RealmId {
187 RealmId::from_known_valid(GLOBAL_REALM_SLUG)
188 }
189}
190
191#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
204#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
205#[serde(rename_all = "snake_case")]
206pub enum BindingOrigin {
207 #[default]
211 Configured,
212 SyntheticEnvDefault,
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
224#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
225pub struct AuthBindingRef {
226 pub realm: RealmId,
227 pub binding: BindingId,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub profile: Option<ProfileId>,
230 #[serde(default, skip_serializing_if = "BindingOrigin::is_configured")]
234 pub origin: BindingOrigin,
235}
236
237impl BindingOrigin {
238 pub fn is_configured(&self) -> bool {
241 matches!(self, BindingOrigin::Configured)
242 }
243}
244
245impl AuthBindingRef {
246 pub fn is_env_default(&self) -> bool {
250 matches!(self.origin, BindingOrigin::SyntheticEnvDefault)
251 }
252}
253
254pub fn mob_realm_id(mob_id: &str) -> Result<RealmId, IdentityError> {
262 RealmId::parse(format!("mob.{mob_id}"))
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, Error)]
267pub enum MemberCommsNameError {
268 #[error(
270 "mob member comms name must have exactly three '/'-separated components (mob_id/role/member)"
271 )]
272 WrongComponentCount,
273 #[error(
275 "mob member comms name component {component:?} is invalid; \
276 each must start with an ASCII letter or '_' and contain only ASCII alphanumerics, '-', or '_'"
277 )]
278 InvalidComponent { component: String },
279}
280
281fn validate_member_comms_name_component(component: &str) -> Result<(), MemberCommsNameError> {
288 let mut chars = component.chars();
289 let Some(first) = chars.next() else {
290 return Err(MemberCommsNameError::InvalidComponent {
291 component: component.to_string(),
292 });
293 };
294 if !first.is_ascii_alphabetic() && first != '_' {
295 return Err(MemberCommsNameError::InvalidComponent {
296 component: component.to_string(),
297 });
298 }
299 if !chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
300 return Err(MemberCommsNameError::InvalidComponent {
301 component: component.to_string(),
302 });
303 }
304 Ok(())
305}
306
307#[derive(Debug, Clone, PartialEq, Eq, Hash)]
320pub struct MemberCommsName {
321 mob_id: String,
322 role: String,
323 member: String,
324}
325
326impl MemberCommsName {
327 pub fn new(
329 mob_id: impl Into<String>,
330 role: impl Into<String>,
331 member: impl Into<String>,
332 ) -> Result<Self, MemberCommsNameError> {
333 let mob_id = mob_id.into();
334 let role = role.into();
335 let member = member.into();
336 validate_member_comms_name_component(&mob_id)?;
337 validate_member_comms_name_component(&role)?;
338 validate_member_comms_name_component(&member)?;
339 Ok(Self {
340 mob_id,
341 role,
342 member,
343 })
344 }
345
346 pub fn mob_id(&self) -> &str {
347 &self.mob_id
348 }
349
350 pub fn role(&self) -> &str {
351 &self.role
352 }
353
354 pub fn member(&self) -> &str {
355 &self.member
356 }
357
358 pub fn to_member_binding(&self) -> MobMemberBinding {
360 MobMemberBinding {
361 mob_id: self.mob_id.clone(),
362 role: self.role.clone(),
363 member: self.member.clone(),
364 }
365 }
366}
367
368impl std::fmt::Display for MemberCommsName {
369 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370 write!(f, "{}/{}/{}", self.mob_id, self.role, self.member)
371 }
372}
373
374impl std::str::FromStr for MemberCommsName {
375 type Err = MemberCommsNameError;
376
377 fn from_str(s: &str) -> Result<Self, Self::Err> {
378 let mut parts = s.split('/');
379 match (parts.next(), parts.next(), parts.next(), parts.next()) {
380 (Some(mob_id), Some(role), Some(member), None) => Self::new(mob_id, role, member),
381 _ => Err(MemberCommsNameError::WrongComponentCount),
382 }
383 }
384}
385
386#[derive(Debug, Clone, PartialEq, Eq)]
393pub enum PeerRole {
394 Member(String),
396 External,
398}
399
400impl PeerRole {
401 pub fn as_label(&self) -> &str {
403 match self {
404 PeerRole::Member(role) => role.as_str(),
405 PeerRole::External => "external",
406 }
407 }
408}
409
410#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
424#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
425#[serde(rename_all = "snake_case")]
426pub struct MobMemberBinding {
427 pub mob_id: String,
428 pub role: String,
429 pub member: String,
430}
431
432impl MobMemberBinding {
433 pub fn comms_name(&self) -> Result<MemberCommsName, MemberCommsNameError> {
435 MemberCommsName::new(self.mob_id.clone(), self.role.clone(), self.member.clone())
436 }
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
441#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
442pub struct BackendProfile {
443 pub id: String,
444 pub provider: Provider,
445 pub backend_kind: String,
446 #[serde(default, skip_serializing_if = "Option::is_none")]
447 pub base_url: Option<String>,
448 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
449 pub options: serde_json::Value,
450}
451
452#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
454#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
455pub struct AuthProfile {
456 pub id: String,
457 pub provider: Provider,
458 pub auth_method: String,
459 pub source: CredentialSourceSpec,
460 #[serde(default)]
461 pub constraints: AuthConstraints,
462 #[serde(default)]
463 pub metadata_defaults: AuthMetadataDefaults,
464}
465
466#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
473#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
474#[serde(transparent)]
475pub struct ExternalResolverId(String);
476
477impl ExternalResolverId {
478 pub fn new(id: impl Into<String>) -> Self {
479 Self(id.into())
480 }
481
482 pub fn as_str(&self) -> &str {
483 &self.0
484 }
485}
486
487impl std::fmt::Display for ExternalResolverId {
488 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
489 f.write_str(&self.0)
490 }
491}
492
493impl From<String> for ExternalResolverId {
494 fn from(value: String) -> Self {
495 Self(value)
496 }
497}
498
499impl From<&str> for ExternalResolverId {
500 fn from(value: &str) -> Self {
501 Self(value.to_string())
502 }
503}
504
505#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
507#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
508#[serde(tag = "kind", rename_all = "snake_case")]
509pub enum CredentialSourceSpec {
510 InlineSecret {
511 secret: String,
512 },
513 ManagedStore,
518 Env {
519 env: String,
520 #[serde(default, skip_serializing_if = "Vec::is_empty")]
526 fallback: Vec<String>,
527 },
528 ExternalResolver {
529 handle: ExternalResolverId,
530 },
531 PlatformDefault,
532 Command {
536 program: PathBuf,
537 #[serde(default)]
538 args: Vec<String>,
539 #[serde(default, skip_serializing_if = "Option::is_none")]
540 cwd: Option<PathBuf>,
541 #[serde(default)]
542 env: BTreeMap<String, String>,
543 #[serde(default = "default_command_timeout_ms")]
545 timeout_ms: u64,
546 #[serde(default, skip_serializing_if = "Option::is_none")]
548 refresh_interval_ms: Option<u64>,
549 },
550 FileDescriptor {
553 fd: i32,
554 #[serde(default, skip_serializing_if = "Option::is_none")]
555 scope_override: Option<String>,
556 },
557}
558
559impl CredentialSourceSpec {
560 pub const ALL_KIND_LABELS: &'static [&'static str] = &[
561 "inline_secret",
562 "managed_store",
563 "env",
564 "external_resolver",
565 "platform_default",
566 "command",
567 "file_descriptor",
568 ];
569
570 pub const fn kind_label(&self) -> &'static str {
571 match self {
572 Self::InlineSecret { .. } => "inline_secret",
573 Self::ManagedStore => "managed_store",
574 Self::Env { .. } => "env",
575 Self::ExternalResolver { .. } => "external_resolver",
576 Self::PlatformDefault => "platform_default",
577 Self::Command { .. } => "command",
578 Self::FileDescriptor { .. } => "file_descriptor",
579 }
580 }
581}
582
583fn default_command_timeout_ms() -> u64 {
584 30_000
585}
586
587#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
589#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
590pub struct BindingPolicy {
591 #[serde(default)]
592 pub allow_auth_override: bool,
593 #[serde(default)]
594 pub require_metadata_account: bool,
595 #[serde(default)]
596 pub require_metadata_workspace: bool,
597}
598
599#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
602#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
603pub struct ProviderBinding {
604 pub id: String,
605 pub backend_profile: String,
606 pub auth_profile: String,
607 #[serde(default, skip_serializing_if = "Option::is_none")]
608 pub default_model: Option<String>,
609 #[serde(default)]
610 pub policy: BindingPolicy,
611 #[serde(default, skip_serializing_if = "is_false")]
617 pub provider_default: bool,
618}
619
620#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
625#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
626pub struct RealmConnectionSet {
627 pub realm_id: RealmId,
628 pub backends: BTreeMap<String, BackendProfile>,
629 pub auth_profiles: BTreeMap<String, AuthProfile>,
630 pub bindings: BTreeMap<String, ProviderBinding>,
631 #[serde(default, skip_serializing_if = "Option::is_none")]
632 pub default_binding: Option<String>,
633}
634
635#[derive(Debug, Clone, PartialEq, Eq)]
639pub struct ResolvedConnectionTarget {
640 pub realm: RealmConnectionSet,
641 pub auth_binding: AuthBindingRef,
642 pub binding: ProviderBinding,
643 pub backend: BackendProfile,
644 pub auth_profile: AuthProfile,
645}
646
647#[derive(Debug, Clone, Error, PartialEq, Eq)]
648pub enum ConnectionTargetError {
649 #[error("connection target did not name a realm and no configured default realm was available")]
650 MissingRealm,
651 #[error("realm '{0}' not found in config.realm")]
652 UnknownRealm(String),
653 #[error("realm '{realm}' has no default binding")]
654 MissingDefaultBinding { realm: String },
655 #[error("invalid realm id '{realm}': {source}")]
656 InvalidRealmId {
657 realm: String,
658 source: IdentityError,
659 },
660 #[error("invalid binding id '{binding}': {source}")]
661 InvalidBindingId {
662 binding: String,
663 source: IdentityError,
664 },
665 #[error("realm '{realm}' config invalid: {source}")]
666 RealmConfigInvalid {
667 realm: String,
668 source: ProviderBindingError,
669 },
670 #[error("binding '{realm}:{binding}' is invalid: {source}")]
671 BindingInvalid {
672 realm: String,
673 binding: String,
674 source: ProviderBindingError,
675 },
676 #[error(
677 "binding '{realm}:{binding}' resolves backend={backend:?} auth={auth:?}, expected provider {expected:?}"
678 )]
679 ProviderMismatch {
680 realm: String,
681 binding: String,
682 expected: Provider,
683 backend: Provider,
684 auth: Provider,
685 },
686 #[error(transparent)]
687 RealmChain(#[from] RealmChainError),
688}
689
690#[derive(Debug, Clone, Error, PartialEq, Eq)]
696pub enum RealmChainError {
697 #[error("realm parent chain has a cycle: {}", .chain.join(" -> "))]
700 Cycle { chain: Vec<String> },
701 #[error("realm parent chain from '{head}' exceeds max depth {max}")]
703 DepthExceeded { head: String, max: usize },
704 #[error("realm '{realm}' names parent '{parent}' which is not configured")]
707 MissingParent { realm: String, parent: String },
708 #[error("the reserved 'global' realm (via '{realm}') must not declare a parent")]
710 GlobalHasParent { realm: String },
711 #[error("realm '{realm}' names the reserved env_default slug as its parent")]
714 ParentIsEnvDefault { realm: String },
715}
716
717#[derive(Debug, Clone, PartialEq, Eq)]
724pub struct RealmChain {
725 realms: Vec<RealmId>,
726}
727
728impl RealmChain {
729 pub fn resolve(config: &Config, head: &RealmId) -> Result<RealmChain, RealmChainError> {
744 let mut ordered: Vec<RealmId> = Vec::new();
745 let mut seen: BTreeSet<RealmId> = BTreeSet::new();
746
747 ordered.push(head.clone());
749 seen.insert(head.clone());
750
751 let mut current = head.clone();
752 loop {
753 if ordered.len() > MAX_REALM_CHAIN_DEPTH {
754 return Err(RealmChainError::DepthExceeded {
755 head: head.as_str().to_string(),
756 max: MAX_REALM_CHAIN_DEPTH,
757 });
758 }
759
760 let current_section = config.realm.get(current.as_str());
762 if current.is_global() && current_section.and_then(|s| s.parent.as_ref()).is_some() {
763 return Err(RealmChainError::GlobalHasParent {
764 realm: current.as_str().to_string(),
765 });
766 }
767
768 let Some(parent) = current_section.and_then(|s| s.parent.clone()) else {
769 if !current.is_global() && config.realm.contains_key(GLOBAL_REALM_SLUG) {
773 let global = RealmId::global();
774 if seen.insert(global.clone()) {
775 ordered.push(global);
776 }
777 }
778 return Ok(RealmChain { realms: ordered });
779 };
780
781 if parent.is_env_default() {
782 return Err(RealmChainError::ParentIsEnvDefault {
783 realm: current.as_str().to_string(),
784 });
785 }
786 if !parent.is_global() && !config.realm.contains_key(parent.as_str()) {
789 return Err(RealmChainError::MissingParent {
790 realm: current.as_str().to_string(),
791 parent: parent.as_str().to_string(),
792 });
793 }
794 if !seen.insert(parent.clone()) {
795 let mut chain: Vec<String> =
796 ordered.iter().map(|r| r.as_str().to_string()).collect();
797 chain.push(parent.as_str().to_string());
798 return Err(RealmChainError::Cycle { chain });
799 }
800 ordered.push(parent.clone());
801 current = parent;
802 }
803 }
804
805 #[must_use]
807 pub fn realms(&self) -> &[RealmId] {
808 &self.realms
809 }
810}
811
812fn env_default_target(
819 provider: Provider,
820 profile: Option<ProfileId>,
821) -> Result<ResolvedConnectionTarget, ConnectionTargetError> {
822 let realm = RealmConnectionSet::synthesize_env_default(provider);
823 let binding =
824 BindingId::parse("default").map_err(|source| ConnectionTargetError::InvalidBindingId {
825 binding: "default".to_string(),
826 source,
827 })?;
828 materialize_connection_target(
829 realm,
830 Some(provider),
831 binding,
832 profile,
833 BindingOrigin::SyntheticEnvDefault,
834 )
835}
836
837fn collect_provider_candidates_on_chain(
856 config: &Config,
857 provider: Provider,
858 head: &RealmId,
859 head_required: bool,
860) -> Result<Vec<ResolvedConnectionTarget>, ConnectionTargetError> {
861 let chain = RealmChain::resolve(config, head)?;
862 let mut out = Vec::new();
863 for (idx, member) in chain.realms().iter().enumerate() {
864 let is_head = idx == 0;
865 let Some(section) = config.realm.get(member.as_str()) else {
866 if is_head && head_required {
867 return Err(ConnectionTargetError::UnknownRealm(
868 member.as_str().to_string(),
869 ));
870 }
871 continue;
872 };
873 let realm = match RealmConnectionSet::from_config(member.as_str(), section) {
874 Ok(realm) => realm,
875 Err(source) => {
876 if is_head && head_required {
877 return Err(ConnectionTargetError::RealmConfigInvalid {
878 realm: member.as_str().to_string(),
879 source,
880 });
881 }
882 continue;
883 }
884 };
885 let binding_id = match selected_binding_id_for_provider(&realm, provider) {
886 Ok(binding_id) => binding_id,
887 Err(err) => {
888 if is_head && head_required {
889 return Err(err);
890 }
891 continue;
892 }
893 };
894 if let Some(binding_id) = binding_id {
895 out.push(materialize_connection_target(
896 realm,
897 Some(provider),
898 binding_id,
899 None,
900 BindingOrigin::Configured,
901 )?);
902 }
903 }
904 Ok(out)
905}
906
907fn resolve_explicit_binding_on_chain(
918 config: &Config,
919 expected_provider: Option<Provider>,
920 head: &RealmId,
921 binding: &BindingId,
922 profile: Option<&ProfileId>,
923 head_required: bool,
924) -> Result<ResolvedConnectionTarget, ConnectionTargetError> {
925 let chain = RealmChain::resolve(config, head)?;
926 for (idx, member) in chain.realms().iter().enumerate() {
927 let is_head = idx == 0;
928 let Some(section) = config.realm.get(member.as_str()) else {
929 if is_head && head_required {
930 return Err(ConnectionTargetError::UnknownRealm(
931 member.as_str().to_string(),
932 ));
933 }
934 continue;
935 };
936 let realm = match RealmConnectionSet::from_config(member.as_str(), section) {
937 Ok(realm) => realm,
938 Err(source) => {
939 if is_head && head_required {
940 return Err(ConnectionTargetError::RealmConfigInvalid {
941 realm: member.as_str().to_string(),
942 source,
943 });
944 }
945 continue;
946 }
947 };
948 if realm.bindings.contains_key(binding.as_str()) {
949 return materialize_connection_target(
950 realm,
951 expected_provider,
952 binding.clone(),
953 profile.cloned(),
954 BindingOrigin::Configured,
955 );
956 }
957 }
958 Err(ConnectionTargetError::BindingInvalid {
959 realm: head.as_str().to_string(),
960 binding: binding.as_str().to_string(),
961 source: ProviderBindingError::UnknownBinding(binding.as_str().to_string()),
962 })
963}
964
965pub fn resolve_explicit_auth_binding_target(
976 config: &Config,
977 auth_binding: &AuthBindingRef,
978) -> Result<ResolvedConnectionTarget, ConnectionTargetError> {
979 if auth_binding.is_env_default() {
980 return Err(ConnectionTargetError::UnknownRealm(
981 auth_binding.realm.as_str().to_string(),
982 ));
983 }
984 resolve_explicit_binding_on_chain(
985 config,
986 None,
987 &auth_binding.realm,
988 &auth_binding.binding,
989 auth_binding.profile.as_ref(),
990 true,
991 )
992}
993
994pub fn resolve_realm_binding_target_for_provider(
1002 config: &Config,
1003 provider: Provider,
1004 explicit_realm: Option<&RealmId>,
1005 explicit_binding: Option<&BindingId>,
1006 explicit_profile: Option<&ProfileId>,
1007 preferred_realm: Option<&RealmId>,
1008 allow_env_default: bool,
1009) -> Result<ResolvedConnectionTarget, ConnectionTargetError> {
1010 let global = RealmId::global();
1016 let head = explicit_realm.or(preferred_realm).unwrap_or(&global);
1017 let head_required = explicit_realm.is_some();
1018
1019 if let Some(binding) = explicit_binding {
1020 return resolve_explicit_binding_on_chain(
1021 config,
1022 Some(provider),
1023 head,
1024 binding,
1025 explicit_profile,
1026 head_required,
1027 );
1028 }
1029 let candidates = collect_provider_candidates_on_chain(config, provider, head, head_required)?;
1030 if let Some(first) = candidates.into_iter().next() {
1031 return Ok(first);
1032 }
1033 if head_required {
1034 return Err(ConnectionTargetError::MissingDefaultBinding {
1035 realm: head.as_str().to_string(),
1036 });
1037 }
1038
1039 if allow_env_default && explicit_realm.is_none() && explicit_binding.is_none() {
1040 return env_default_target(provider, explicit_profile.cloned());
1041 }
1042
1043 Err(ConnectionTargetError::MissingRealm)
1044}
1045
1046pub fn resolve_auth_binding_or_default_for_provider(
1050 config: &Config,
1051 provider: Provider,
1052 auth_binding: Option<&AuthBindingRef>,
1053 preferred_realm: Option<&RealmId>,
1054 allow_env_default: bool,
1055) -> Result<ResolvedConnectionTarget, ConnectionTargetError> {
1056 if let Some(auth_binding) = auth_binding {
1057 if auth_binding.is_env_default() {
1060 return Err(ConnectionTargetError::UnknownRealm(
1061 auth_binding.realm.as_str().to_string(),
1062 ));
1063 }
1064 return resolve_explicit_binding_on_chain(
1067 config,
1068 Some(provider),
1069 &auth_binding.realm,
1070 &auth_binding.binding,
1071 auth_binding.profile.as_ref(),
1072 true,
1073 );
1074 }
1075
1076 resolve_realm_binding_target_for_provider(
1077 config,
1078 provider,
1079 None,
1080 None,
1081 None,
1082 preferred_realm,
1083 allow_env_default,
1084 )
1085}
1086
1087fn selected_binding_id_for_provider(
1088 realm: &RealmConnectionSet,
1089 provider: Provider,
1090) -> Result<Option<BindingId>, ConnectionTargetError> {
1091 let mut provider_bindings = Vec::new();
1092 let mut provider_default_binding: Option<&str> = None;
1093 for (binding_id, binding) in &realm.bindings {
1094 let backend = realm
1095 .backends
1096 .get(&binding.backend_profile)
1097 .ok_or_else(|| ConnectionTargetError::BindingInvalid {
1098 realm: realm.realm_id.to_string(),
1099 binding: binding_id.clone(),
1100 source: ProviderBindingError::UnknownBackend(binding.backend_profile.clone()),
1101 })?;
1102 let auth = realm
1103 .auth_profiles
1104 .get(&binding.auth_profile)
1105 .ok_or_else(|| ConnectionTargetError::BindingInvalid {
1106 realm: realm.realm_id.to_string(),
1107 binding: binding_id.clone(),
1108 source: ProviderBindingError::UnknownAuth(binding.auth_profile.clone()),
1109 })?;
1110 if backend.provider == provider && auth.provider == provider {
1111 provider_bindings.push(binding_id.as_str());
1112 if binding.provider_default && provider_default_binding.is_none() {
1116 provider_default_binding = Some(binding_id.as_str());
1117 }
1118 }
1119 }
1120
1121 if let Some(default_binding) = realm.default_binding.as_deref()
1122 && provider_bindings.contains(&default_binding)
1123 {
1124 return BindingId::parse(default_binding.to_string())
1125 .map(Some)
1126 .map_err(|source| ConnectionTargetError::InvalidBindingId {
1127 binding: default_binding.to_string(),
1128 source,
1129 });
1130 }
1131
1132 if let Some(provider_default_binding) = provider_default_binding {
1133 return BindingId::parse(provider_default_binding.to_string())
1134 .map(Some)
1135 .map_err(|source| ConnectionTargetError::InvalidBindingId {
1136 binding: provider_default_binding.to_string(),
1137 source,
1138 });
1139 }
1140
1141 match provider_bindings.as_slice() {
1142 [binding_id] => BindingId::parse((*binding_id).to_string())
1143 .map(Some)
1144 .map_err(|source| ConnectionTargetError::InvalidBindingId {
1145 binding: (*binding_id).to_string(),
1146 source,
1147 }),
1148 _ => Ok(None),
1149 }
1150}
1151
1152pub fn resolve_auth_binding_candidates_for_provider(
1169 config: &Config,
1170 provider: Provider,
1171 auth_binding: Option<&AuthBindingRef>,
1172 preferred_realm: Option<&RealmId>,
1173 allow_env_default: bool,
1174) -> Result<Vec<ResolvedConnectionTarget>, ConnectionTargetError> {
1175 if auth_binding.is_some() {
1176 return resolve_auth_binding_or_default_for_provider(
1177 config,
1178 provider,
1179 auth_binding,
1180 preferred_realm,
1181 allow_env_default,
1182 )
1183 .map(|target| vec![target]);
1184 }
1185
1186 let global = RealmId::global();
1191 let head = preferred_realm.unwrap_or(&global);
1192 let mut candidates = Vec::new();
1193 candidates.extend(collect_provider_candidates_on_chain(
1194 config, provider, head, false,
1195 )?);
1196
1197 if allow_env_default {
1198 candidates.push(env_default_target(provider, None)?);
1199 }
1200
1201 if candidates.is_empty() {
1202 return Err(ConnectionTargetError::MissingRealm);
1203 }
1204 Ok(candidates)
1205}
1206
1207#[derive(Debug, Clone, Error, PartialEq, Eq)]
1215pub enum WriteOwnerError {
1216 #[error(
1219 "binding '{binding}' is inherited by realm '{head}' from its owning realm '{owner}'; \
1220 credential reads inherit down the chain, but writes are strict-owner — target the \
1221 owning realm '{owner}', not '{head}'"
1222 )]
1223 Inherited {
1224 binding: String,
1225 head: String,
1226 owner: String,
1227 },
1228 #[error("binding '{binding}' is not defined on realm '{head}' or any realm it inherits from")]
1230 Unknown { binding: String, head: String },
1231 #[error(transparent)]
1233 Chain(#[from] RealmChainError),
1234}
1235
1236pub fn resolve_write_owner(
1243 config: &Config,
1244 head: &RealmId,
1245 binding: &BindingId,
1246) -> Result<RealmId, WriteOwnerError> {
1247 let defines = |realm: &RealmId| {
1248 config
1249 .realm
1250 .get(realm.as_str())
1251 .is_some_and(|section| section.binding.contains_key(binding.as_str()))
1252 };
1253 if defines(head) {
1254 return Ok(head.clone());
1255 }
1256 let chain = RealmChain::resolve(config, head)?;
1257 if let Some(owner) = chain.realms().iter().skip(1).find(|member| defines(member)) {
1259 return Err(WriteOwnerError::Inherited {
1260 binding: binding.as_str().to_string(),
1261 head: head.as_str().to_string(),
1262 owner: owner.as_str().to_string(),
1263 });
1264 }
1265 Err(WriteOwnerError::Unknown {
1266 binding: binding.as_str().to_string(),
1267 head: head.as_str().to_string(),
1268 })
1269}
1270
1271fn materialize_connection_target(
1272 realm: RealmConnectionSet,
1273 expected_provider: Option<Provider>,
1274 binding: BindingId,
1275 profile: Option<ProfileId>,
1276 origin: BindingOrigin,
1277) -> Result<ResolvedConnectionTarget, ConnectionTargetError> {
1278 let auth_binding = AuthBindingRef {
1281 realm: realm.realm_id.clone(),
1282 binding,
1283 profile,
1284 origin,
1285 };
1286 let (binding, backend, auth_profile) =
1287 realm.lookup_auth_binding(&auth_binding).map_err(|source| {
1288 ConnectionTargetError::BindingInvalid {
1289 realm: auth_binding.realm.to_string(),
1290 binding: auth_binding.binding.to_string(),
1291 source,
1292 }
1293 })?;
1294 let provider = expected_provider.unwrap_or(backend.provider);
1295 if backend.provider != provider || auth_profile.provider != provider {
1296 return Err(ConnectionTargetError::ProviderMismatch {
1297 realm: auth_binding.realm.to_string(),
1298 binding: auth_binding.binding.to_string(),
1299 expected: provider,
1300 backend: backend.provider,
1301 auth: auth_profile.provider,
1302 });
1303 }
1304 let binding = binding.clone();
1305 let backend = backend.clone();
1306 let auth_profile = auth_profile.clone();
1307 Ok(ResolvedConnectionTarget {
1308 realm,
1309 auth_binding,
1310 binding,
1311 backend,
1312 auth_profile,
1313 })
1314}
1315
1316impl RealmConnectionSet {
1317 pub fn from_config(
1322 realm_id: &str,
1323 section: &RealmConfigSection,
1324 ) -> Result<Self, ProviderBindingError> {
1325 let realm_id =
1326 RealmId::parse(realm_id).map_err(|source| ProviderBindingError::InvalidRealmId {
1327 realm: realm_id.to_string(),
1328 source,
1329 })?;
1330 let mut backends: BTreeMap<String, BackendProfile> = BTreeMap::new();
1331 for (id, cfg) in §ion.backend {
1332 let provider = Provider::parse_strict(&cfg.provider)
1333 .ok_or_else(|| ProviderBindingError::UnknownProviderName(cfg.provider.clone()))?;
1334 let backend = BackendProfile {
1335 id: id.clone(),
1336 provider,
1337 backend_kind: cfg.backend_kind.clone(),
1338 base_url: cfg.base_url.clone(),
1339 options: cfg.options.clone(),
1340 };
1341 backends.insert(id.clone(), backend);
1344 }
1345
1346 let mut auth_profiles: BTreeMap<String, AuthProfile> = BTreeMap::new();
1347 for (id, cfg) in §ion.auth {
1348 let provider = Provider::parse_strict(&cfg.provider)
1349 .ok_or_else(|| ProviderBindingError::UnknownProviderName(cfg.provider.clone()))?;
1350 let profile = AuthProfile {
1351 id: id.clone(),
1352 provider,
1353 auth_method: cfg.auth_method.clone(),
1354 source: cfg.source.clone(),
1355 constraints: cfg.constraints.clone(),
1356 metadata_defaults: cfg.metadata_defaults.clone(),
1357 };
1358 auth_profiles.insert(id.clone(), profile);
1359 }
1360
1361 let mut bindings: BTreeMap<String, ProviderBinding> = BTreeMap::new();
1362 for (id, cfg) in §ion.binding {
1363 let backend = backends
1364 .get(&cfg.backend_profile)
1365 .ok_or_else(|| ProviderBindingError::UnknownBackend(cfg.backend_profile.clone()))?;
1366 let auth = auth_profiles
1367 .get(&cfg.auth_profile)
1368 .ok_or_else(|| ProviderBindingError::UnknownAuth(cfg.auth_profile.clone()))?;
1369 if backend.provider != auth.provider {
1370 return Err(ProviderBindingError::ProviderMismatch {
1371 binding: id.clone(),
1372 backend: backend.provider,
1373 auth: auth.provider,
1374 });
1375 }
1376 let binding = ProviderBinding {
1377 id: id.clone(),
1378 backend_profile: cfg.backend_profile.clone(),
1379 auth_profile: cfg.auth_profile.clone(),
1380 default_model: cfg.default_model.clone(),
1381 policy: cfg.policy.clone(),
1382 provider_default: cfg.provider_default,
1383 };
1384 bindings.insert(id.clone(), binding);
1385 }
1386
1387 Ok(Self {
1388 realm_id,
1389 backends,
1390 auth_profiles,
1391 bindings,
1392 default_binding: section.default_binding.clone(),
1393 })
1394 }
1395
1396 pub fn synthesize_env_default(provider: Provider) -> Self {
1424 Self::synthesize_env_default_from_lookup(provider, |key| std::env::var(key).ok())
1425 }
1426
1427 pub fn synthesize_env_default_from_lookup<F>(provider: Provider, env_lookup: F) -> Self
1430 where
1431 F: Fn(&str) -> Option<String>,
1432 {
1433 let spec = env_default_spec(provider, env_lookup);
1434 Self::synthesize_default_from_spec(provider, spec)
1435 }
1436
1437 fn synthesize_default_from_spec(provider: Provider, spec: EnvDefaultSpec) -> Self {
1438 let backend = BackendProfile {
1439 id: "default".to_string(),
1440 provider,
1441 backend_kind: spec.backend_kind.to_string(),
1442 base_url: spec.base_url,
1443 options: spec.options,
1444 };
1445 let source = CredentialSourceSpec::Env {
1446 env: spec.env_var.to_string(),
1447 fallback: spec.fallback,
1448 };
1449 let auth = AuthProfile {
1450 id: "default".to_string(),
1451 provider,
1452 auth_method: spec.auth_method.to_string(),
1453 source,
1454 constraints: AuthConstraints::default(),
1455 metadata_defaults: AuthMetadataDefaults::default(),
1456 };
1457 let binding = ProviderBinding {
1458 id: "default".to_string(),
1459 backend_profile: "default".to_string(),
1460 auth_profile: "default".to_string(),
1461 default_model: None,
1462 policy: BindingPolicy::default(),
1463 provider_default: true,
1464 };
1465 let mut backends = BTreeMap::new();
1466 backends.insert("default".to_string(), backend);
1467 let mut auth_profiles = BTreeMap::new();
1468 auth_profiles.insert("default".to_string(), auth);
1469 let mut bindings = BTreeMap::new();
1470 bindings.insert("default".to_string(), binding);
1471 Self {
1472 realm_id: RealmId::from_known_valid(ENV_DEFAULT_REALM_SLUG),
1473 backends,
1474 auth_profiles,
1475 bindings,
1476 default_binding: Some("default".to_string()),
1477 }
1478 }
1479
1480 pub fn lookup_binding(
1483 &self,
1484 id: &str,
1485 ) -> Result<(&ProviderBinding, &BackendProfile, &AuthProfile), ProviderBindingError> {
1486 let binding = self
1487 .bindings
1488 .get(id)
1489 .ok_or_else(|| ProviderBindingError::UnknownBinding(id.to_string()))?;
1490 let backend = self
1491 .backends
1492 .get(&binding.backend_profile)
1493 .ok_or_else(|| ProviderBindingError::UnknownBackend(binding.backend_profile.clone()))?;
1494 let auth = self
1495 .auth_profiles
1496 .get(&binding.auth_profile)
1497 .ok_or_else(|| ProviderBindingError::UnknownAuth(binding.auth_profile.clone()))?;
1498 Ok((binding, backend, auth))
1499 }
1500
1501 pub fn lookup_auth_binding(
1505 &self,
1506 auth_binding: &AuthBindingRef,
1507 ) -> Result<(&ProviderBinding, &BackendProfile, &AuthProfile), ProviderBindingError> {
1508 let binding = self
1509 .bindings
1510 .get(auth_binding.binding.as_str())
1511 .ok_or_else(|| {
1512 ProviderBindingError::UnknownBinding(auth_binding.binding.to_string())
1513 })?;
1514 let backend = self
1515 .backends
1516 .get(&binding.backend_profile)
1517 .ok_or_else(|| ProviderBindingError::UnknownBackend(binding.backend_profile.clone()))?;
1518 let auth_profile_id = auth_binding
1519 .profile
1520 .as_ref()
1521 .map(ProfileId::as_str)
1522 .unwrap_or(binding.auth_profile.as_str());
1523 let auth = self
1524 .auth_profiles
1525 .get(auth_profile_id)
1526 .ok_or_else(|| ProviderBindingError::UnknownAuth(auth_profile_id.to_string()))?;
1527 Ok((binding, backend, auth))
1528 }
1529}
1530
1531#[derive(Debug, Clone, Error, Serialize, Deserialize, PartialEq, Eq)]
1541#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1542#[serde(tag = "kind", rename_all = "snake_case")]
1543pub enum ProviderBindingError {
1544 #[error("unknown binding: {0}")]
1545 UnknownBinding(String),
1546 #[error("unknown backend: {0}")]
1547 UnknownBackend(String),
1548 #[error("unknown auth: {0}")]
1549 UnknownAuth(String),
1550 #[error("provider mismatch on binding {binding}: backend={backend:?} auth={auth:?}")]
1551 ProviderMismatch {
1552 binding: String,
1553 backend: Provider,
1554 auth: Provider,
1555 },
1556 #[error("unknown provider name: {0}")]
1557 UnknownProviderName(String),
1558 #[error("invalid realm id '{realm}': {source}")]
1559 InvalidRealmId {
1560 realm: String,
1561 source: IdentityError,
1562 },
1563}
1564
1565#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1575#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1576pub struct RealmConfigSection {
1577 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1578 pub backend: BTreeMap<String, BackendProfileConfig>,
1579 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1580 pub auth: BTreeMap<String, AuthProfileConfig>,
1581 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1582 pub binding: BTreeMap<String, ProviderBindingConfig>,
1583 #[serde(default, skip_serializing_if = "Option::is_none")]
1584 pub default_binding: Option<String>,
1585 #[serde(default, skip_serializing_if = "Option::is_none")]
1592 pub parent: Option<RealmId>,
1593}
1594
1595impl RealmConfigSection {
1596 pub fn from_inline_api_keys(entries: &[(&str, &str)]) -> Self {
1614 let mut backend = BTreeMap::new();
1615 let mut auth = BTreeMap::new();
1616 let mut binding = BTreeMap::new();
1617 let mut default_binding: Option<String> = None;
1618
1619 for (idx, (provider, secret)) in entries.iter().enumerate() {
1620 let id = format!("default_{provider}");
1621 let (backend_kind, auth_method) = match *provider {
1627 "anthropic" => (
1628 AnthropicBackendKind::AnthropicApi.as_str(),
1629 AnthropicAuthMethod::ApiKey.as_str(),
1630 ),
1631 "openai" => (
1632 OpenAiBackendKind::OpenAiApi.as_str(),
1633 OpenAiAuthMethod::ApiKey.as_str(),
1634 ),
1635 "gemini" | "google" => (
1636 GoogleBackendKind::GoogleGenAi.as_str(),
1637 GoogleAuthMethod::ApiKey.as_str(),
1638 ),
1639 other => (other, "api_key"),
1640 };
1641 backend.insert(
1642 id.clone(),
1643 BackendProfileConfig {
1644 provider: provider.to_string(),
1645 backend_kind: backend_kind.to_string(),
1646 base_url: None,
1647 options: serde_json::Value::Null,
1648 },
1649 );
1650 auth.insert(
1651 id.clone(),
1652 AuthProfileConfig {
1653 provider: provider.to_string(),
1654 auth_method: auth_method.to_string(),
1655 source: CredentialSourceSpec::InlineSecret {
1656 secret: (*secret).to_string(),
1657 },
1658 constraints: AuthConstraints::default(),
1659 metadata_defaults: AuthMetadataDefaults::default(),
1660 },
1661 );
1662 binding.insert(
1663 id.clone(),
1664 ProviderBindingConfig {
1665 backend_profile: id.clone(),
1666 auth_profile: id.clone(),
1667 default_model: None,
1668 policy: BindingPolicy::default(),
1669 provider_default: true,
1673 },
1674 );
1675 if idx == 0 {
1676 default_binding = Some(id);
1678 }
1679 }
1680
1681 Self {
1682 backend,
1683 auth,
1684 binding,
1685 default_binding,
1686 parent: None,
1687 }
1688 }
1689}
1690
1691fn env_default_spec<F>(provider: Provider, env_lookup: F) -> EnvDefaultSpec
1692where
1693 F: Fn(&str) -> Option<String>,
1694{
1695 match provider {
1696 Provider::Anthropic => EnvDefaultSpec {
1697 backend_kind: AnthropicBackendKind::AnthropicApi.as_str(),
1698 auth_method: AnthropicAuthMethod::ApiKey.as_str(),
1699 env_var: "ANTHROPIC_API_KEY",
1700 fallback: vec![],
1701 base_url: None,
1702 options: serde_json::Value::Null,
1703 },
1704 Provider::OpenAI => openai_env_default_spec(env_lookup),
1705 Provider::Gemini => EnvDefaultSpec {
1706 backend_kind: GoogleBackendKind::GoogleGenAi.as_str(),
1707 auth_method: GoogleAuthMethod::ApiKey.as_str(),
1708 env_var: "GEMINI_API_KEY",
1709 fallback: vec!["GOOGLE_API_KEY".to_string()],
1710 base_url: None,
1711 options: serde_json::Value::Null,
1712 },
1713 Provider::SelfHosted => EnvDefaultSpec {
1714 backend_kind: SelfHostedBackendKind::SelfHosted.as_str(),
1715 auth_method: SelfHostedAuthMethod::ApiKey.as_str(),
1716 env_var: "RKAT_SELF_HOSTED_API_KEY",
1717 fallback: vec![],
1718 base_url: None,
1719 options: serde_json::Value::Null,
1720 },
1721 Provider::Other => EnvDefaultSpec {
1726 backend_kind: "other_api",
1727 auth_method: "api_key",
1728 env_var: "RKAT_OTHER_API_KEY",
1729 fallback: vec![],
1730 base_url: None,
1731 options: serde_json::Value::Null,
1732 },
1733 }
1734}
1735
1736fn openai_env_default_spec<F>(env_lookup: F) -> EnvDefaultSpec
1737where
1738 F: Fn(&str) -> Option<String>,
1739{
1740 let public_openai_key = env_value_with_rkat(&env_lookup, "OPENAI_API_KEY");
1741 let azure_key = env_value_with_rkat(&env_lookup, AZURE_OPENAI_API_KEY_ENV);
1742 let azure_endpoint = env_value_with_rkat(&env_lookup, AZURE_OPENAI_ENDPOINT_ENV);
1743 let azure_explicit = direct_env_value(&env_lookup, &format!("RKAT_{AZURE_OPENAI_API_KEY_ENV}"))
1744 .is_some()
1745 || direct_env_value(&env_lookup, &format!("RKAT_{AZURE_OPENAI_ENDPOINT_ENV}")).is_some();
1746 if azure_key.is_some()
1747 && let Some(endpoint) = azure_endpoint
1748 && (azure_explicit || public_openai_key.is_none())
1749 {
1750 let mut options = serde_json::Map::new();
1751 if let Some(deployment) =
1752 env_value_with_rkat(&env_lookup, AZURE_OPENAI_IMAGE_GENERATION_DEPLOYMENT_ENV)
1753 .or_else(|| env_value_with_rkat(&env_lookup, AZURE_OPENAI_IMAGE_DEPLOYMENT_ENV))
1754 {
1755 options.insert(
1756 "image_generation_deployment".to_string(),
1757 serde_json::Value::String(deployment),
1758 );
1759 }
1760 if let Some(api_version) =
1761 env_value_with_rkat(&env_lookup, AZURE_OPENAI_IMAGE_GENERATION_API_VERSION_ENV)
1762 {
1763 options.insert(
1764 "image_generation_api_version".to_string(),
1765 serde_json::Value::String(api_version),
1766 );
1767 }
1768 return EnvDefaultSpec {
1769 backend_kind: OpenAiBackendKind::AzureOpenAi.as_str(),
1770 auth_method: OpenAiAuthMethod::AzureApiKey.as_str(),
1771 env_var: AZURE_OPENAI_API_KEY_ENV,
1772 fallback: vec![],
1773 base_url: Some(endpoint),
1774 options: if options.is_empty() {
1775 serde_json::Value::Null
1776 } else {
1777 serde_json::Value::Object(options)
1778 },
1779 };
1780 }
1781 EnvDefaultSpec {
1782 backend_kind: OpenAiBackendKind::OpenAiApi.as_str(),
1783 auth_method: OpenAiAuthMethod::ApiKey.as_str(),
1784 env_var: "OPENAI_API_KEY",
1785 fallback: vec![],
1786 base_url: None,
1787 options: serde_json::Value::Null,
1788 }
1789}
1790
1791fn env_value_with_rkat<F>(env_lookup: &F, candidate: &str) -> Option<String>
1792where
1793 F: Fn(&str) -> Option<String>,
1794{
1795 let rkat_override = if candidate.starts_with("RKAT_") {
1796 None
1797 } else {
1798 direct_env_value(env_lookup, &format!("RKAT_{candidate}"))
1799 };
1800 rkat_override.or_else(|| direct_env_value(env_lookup, candidate))
1801}
1802
1803fn direct_env_value<F>(env_lookup: &F, key: &str) -> Option<String>
1804where
1805 F: Fn(&str) -> Option<String>,
1806{
1807 env_lookup(key)
1808 .map(|value| value.trim().to_string())
1809 .filter(|value| !value.is_empty())
1810}
1811
1812#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1814#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1815pub struct BackendProfileConfig {
1816 pub provider: String,
1817 pub backend_kind: String,
1818 #[serde(default, skip_serializing_if = "Option::is_none")]
1819 pub base_url: Option<String>,
1820 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
1821 pub options: serde_json::Value,
1822}
1823
1824#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1826#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1827pub struct AuthProfileConfig {
1828 pub provider: String,
1829 pub auth_method: String,
1830 pub source: CredentialSourceSpec,
1831 #[serde(default)]
1832 pub constraints: AuthConstraints,
1833 #[serde(default)]
1834 pub metadata_defaults: AuthMetadataDefaults,
1835}
1836
1837#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1839#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1840pub struct ProviderBindingConfig {
1841 pub backend_profile: String,
1842 pub auth_profile: String,
1843 #[serde(default, skip_serializing_if = "Option::is_none")]
1844 pub default_model: Option<String>,
1845 #[serde(default)]
1846 pub policy: BindingPolicy,
1847 #[serde(default, skip_serializing_if = "is_false")]
1850 pub provider_default: bool,
1851}
1852
1853#[cfg(test)]
1854#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
1855mod tests {
1856 use super::*;
1857 use std::str::FromStr;
1858
1859 fn rid(s: &str) -> RealmId {
1862 RealmId::parse(s).expect("valid realm slug")
1863 }
1864
1865 fn config_with(realms: &[(&str, Option<&str>)]) -> Config {
1867 let mut cfg = Config::default();
1868 for (id, parent) in realms {
1869 cfg.realm.insert(
1870 (*id).to_string(),
1871 RealmConfigSection {
1872 parent: parent.map(rid),
1873 ..Default::default()
1874 },
1875 );
1876 }
1877 cfg
1878 }
1879
1880 fn chain_ids(chain: &RealmChain) -> Vec<&str> {
1881 chain.realms().iter().map(RealmId::as_str).collect()
1882 }
1883
1884 #[test]
1886 fn realm_config_section_parent_roundtrips_and_defaults_none() {
1887 let with_parent = RealmConfigSection {
1888 parent: Some(RealmId::global()),
1889 ..Default::default()
1890 };
1891 let serialized = toml::to_string(&with_parent).expect("serialize section");
1892 let back: RealmConfigSection = toml::from_str(&serialized).expect("parse section");
1893 assert_eq!(back.parent, Some(RealmId::global()));
1894
1895 let bare: RealmConfigSection = toml::from_str("").expect("parse empty section");
1896 assert_eq!(bare.parent, None, "absent parent must default to None");
1897 }
1898
1899 #[test]
1901 fn global_realm_is_typed_and_distinct_from_env_default() {
1902 let global = RealmId::global();
1903 assert!(global.is_global());
1904 assert!(!global.is_env_default());
1905 assert_eq!(global.as_str(), GLOBAL_REALM_SLUG);
1906
1907 let env = RealmId::from_known_valid(ENV_DEFAULT_REALM_SLUG);
1908 assert!(env.is_env_default());
1909 assert!(!env.is_global());
1910
1911 let other = rid("prod");
1912 assert!(!other.is_global());
1913 assert!(!other.is_env_default());
1914 }
1915
1916 #[test]
1918 fn realm_chain_resolves_linear_order_with_implicit_global_tail() {
1919 let cfg = config_with(&[("child", Some("team")), ("team", None), ("global", None)]);
1921 let chain = RealmChain::resolve(&cfg, &rid("child")).expect("resolve");
1922 assert_eq!(chain_ids(&chain), ["child", "team", "global"]);
1923
1924 let cfg = config_with(&[("child", Some("global")), ("global", None)]);
1926 let chain = RealmChain::resolve(&cfg, &rid("child")).expect("resolve");
1927 assert_eq!(chain_ids(&chain), ["child", "global"]);
1928
1929 let cfg = config_with(&[("global", None)]);
1931 let chain = RealmChain::resolve(&cfg, &rid("global")).expect("resolve");
1932 assert_eq!(chain_ids(&chain), ["global"]);
1933 }
1934
1935 #[test]
1937 fn realm_chain_detects_cycle_depth_missing_global_and_env_default() {
1938 let cfg = config_with(&[("a", Some("a"))]);
1940 assert!(matches!(
1941 RealmChain::resolve(&cfg, &rid("a")),
1942 Err(RealmChainError::Cycle { .. })
1943 ));
1944
1945 let cfg = config_with(&[("a", Some("b")), ("b", Some("a"))]);
1947 assert!(matches!(
1948 RealmChain::resolve(&cfg, &rid("a")),
1949 Err(RealmChainError::Cycle { .. })
1950 ));
1951
1952 let cfg = config_with(&[("a", Some("ghost"))]);
1954 assert!(matches!(
1955 RealmChain::resolve(&cfg, &rid("a")),
1956 Err(RealmChainError::MissingParent { .. })
1957 ));
1958
1959 let cfg = config_with(&[("global", Some("x")), ("x", None)]);
1961 assert!(matches!(
1962 RealmChain::resolve(&cfg, &rid("global")),
1963 Err(RealmChainError::GlobalHasParent { .. })
1964 ));
1965
1966 let cfg = config_with(&[
1968 ("child", Some("global")),
1969 ("global", Some("x")),
1970 ("x", None),
1971 ]);
1972 assert!(matches!(
1973 RealmChain::resolve(&cfg, &rid("child")),
1974 Err(RealmChainError::GlobalHasParent { .. })
1975 ));
1976
1977 let cfg = config_with(&[("a", Some(ENV_DEFAULT_REALM_SLUG))]);
1979 assert!(matches!(
1980 RealmChain::resolve(&cfg, &rid("a")),
1981 Err(RealmChainError::ParentIsEnvDefault { .. })
1982 ));
1983
1984 let mut pairs: Vec<(String, Option<String>)> = Vec::new();
1986 let n = MAX_REALM_CHAIN_DEPTH + 4;
1987 for i in 0..n {
1988 let parent = if i + 1 < n {
1989 Some(format!("r{}", i + 1))
1990 } else {
1991 None
1992 };
1993 pairs.push((format!("r{i}"), parent));
1994 }
1995 let mut cfg = Config::default();
1996 for (id, parent) in &pairs {
1997 cfg.realm.insert(
1998 id.clone(),
1999 RealmConfigSection {
2000 parent: parent.as_deref().map(rid),
2001 ..Default::default()
2002 },
2003 );
2004 }
2005 assert!(matches!(
2006 RealmChain::resolve(&cfg, &rid("r0")),
2007 Err(RealmChainError::DepthExceeded { .. })
2008 ));
2009 }
2010
2011 #[test]
2013 fn realm_chain_omits_absent_global_and_terminates_at_explicit_root() {
2014 let cfg = config_with(&[("a", Some("b")), ("b", None)]);
2016 let chain = RealmChain::resolve(&cfg, &rid("a")).expect("resolve");
2017 assert_eq!(
2018 chain_ids(&chain),
2019 ["a", "b"],
2020 "no global must not be invented"
2021 );
2022 }
2023
2024 #[test]
2026 fn absent_head_realm_yields_single_node_chain_then_implicit_tail() {
2027 let cfg = config_with(&[("global", None)]);
2029 let chain = RealmChain::resolve(&cfg, &rid("missing")).expect("resolve");
2030 assert_eq!(chain_ids(&chain), ["missing", "global"]);
2031
2032 let cfg = Config::default();
2035 let chain = RealmChain::resolve(&cfg, &rid("missing")).expect("resolve");
2036 assert_eq!(chain_ids(&chain), ["missing"]);
2037 }
2038
2039 #[test]
2040 fn member_comms_name_round_trips_through_display_and_from_str() {
2041 let name = MemberCommsName::new("team", "reviewer", "alice").unwrap();
2042 assert_eq!(name.to_string(), "team/reviewer/alice");
2043 let parsed = MemberCommsName::from_str("team/reviewer/alice").unwrap();
2044 assert_eq!(parsed, name);
2045 assert_eq!(parsed.mob_id(), "team");
2046 assert_eq!(parsed.role(), "reviewer");
2047 assert_eq!(parsed.member(), "alice");
2048 }
2049
2050 #[test]
2051 fn member_comms_name_from_str_is_fail_closed() {
2052 assert!(matches!(
2054 MemberCommsName::from_str("team/reviewer"),
2055 Err(MemberCommsNameError::WrongComponentCount)
2056 ));
2057 assert!(matches!(
2058 MemberCommsName::from_str("team/reviewer/alice/extra"),
2059 Err(MemberCommsNameError::WrongComponentCount)
2060 ));
2061 assert!(matches!(
2063 MemberCommsName::from_str("team//alice"),
2064 Err(MemberCommsNameError::InvalidComponent { .. })
2065 ));
2066 assert!(MemberCommsName::from_str("1team/reviewer/alice").is_err());
2068 assert!(MemberCommsName::from_str("te.am/reviewer/alice").is_err());
2070 assert!(MemberCommsName::from_str("_team/reviewer/alice").is_ok());
2072 }
2073
2074 #[test]
2075 fn member_comms_name_components_are_always_valid_realm_slugs() {
2076 let name = MemberCommsName::new("team", "reviewer", "alice").unwrap();
2079 assert!(mob_realm_id(name.mob_id()).is_ok());
2080 assert_eq!(mob_realm_id("team").unwrap().as_str(), "mob.team");
2081 }
2082
2083 #[test]
2084 fn mob_member_binding_round_trips_to_comms_name() {
2085 let binding = MobMemberBinding {
2086 mob_id: "team".to_string(),
2087 role: "reviewer".to_string(),
2088 member: "alice".to_string(),
2089 };
2090 assert_eq!(
2091 binding.comms_name().unwrap().to_string(),
2092 "team/reviewer/alice"
2093 );
2094 }
2095
2096 #[test]
2097 fn peer_role_external_label_is_typed_not_magic_string() {
2098 assert_eq!(PeerRole::External.as_label(), "external");
2099 assert_eq!(
2100 PeerRole::Member("reviewer".to_string()).as_label(),
2101 "reviewer"
2102 );
2103 }
2104
2105 fn config_with_realms(toml_input: &str) -> Config {
2106 Config {
2107 realm: toml::from_str(toml_input).unwrap(),
2108 ..Default::default()
2109 }
2110 }
2111
2112 fn openai_target_config() -> Config {
2113 config_with_realms(
2114 r#"
2115[prod]
2116default_binding = "primary"
2117
2118[prod.backend.openai_default]
2119provider = "openai"
2120backend_kind = "openai_api"
2121
2122[prod.auth.openai_oauth]
2123provider = "openai"
2124auth_method = "chatgpt_oauth"
2125source = { kind = "platform_default" }
2126
2127[prod.binding.primary]
2128backend_profile = "openai_default"
2129auth_profile = "openai_oauth"
2130
2131[prod.binding.secondary]
2132backend_profile = "openai_default"
2133auth_profile = "openai_oauth"
2134"#,
2135 )
2136 }
2137
2138 fn lookup_from_pairs(
2139 pairs: &'static [(&'static str, &'static str)],
2140 ) -> impl Fn(&str) -> Option<String> {
2141 move |key| {
2142 pairs
2143 .iter()
2144 .find_map(|(candidate, value)| (*candidate == key).then(|| (*value).to_string()))
2145 }
2146 }
2147
2148 #[test]
2149 fn auth_binding_is_purely_structural() {
2150 let c = AuthBindingRef {
2151 realm: RealmId::parse("dev").unwrap(),
2152 binding: BindingId::parse("default_openai").unwrap(),
2153 profile: None,
2154 origin: BindingOrigin::Configured,
2155 };
2156 assert_eq!(c.realm.as_str(), "dev");
2157 assert_eq!(c.binding.as_str(), "default_openai");
2158 assert!(c.profile.is_none());
2159 assert!(!c.is_env_default());
2160 }
2161
2162 #[test]
2163 fn auth_binding_serde_roundtrip_with_profile() {
2164 let c = AuthBindingRef {
2165 realm: RealmId::parse("prod").unwrap(),
2166 binding: BindingId::parse("gpt5").unwrap(),
2167 profile: Some(ProfileId::parse("override").unwrap()),
2168 origin: BindingOrigin::Configured,
2169 };
2170 let s = serde_json::to_string(&c).unwrap();
2171 assert!(s.contains("\"realm\":\"prod\""));
2172 assert!(s.contains("\"binding\":\"gpt5\""));
2173 assert!(s.contains("\"profile\":\"override\""));
2174 assert!(!s.contains("origin"));
2177 let back: AuthBindingRef = serde_json::from_str(&s).unwrap();
2178 assert_eq!(back, c);
2179 }
2180
2181 #[test]
2182 fn auth_binding_origin_is_typed_not_slug() {
2183 let synthetic = AuthBindingRef {
2186 realm: RealmId::parse("env_default").unwrap(),
2187 binding: BindingId::parse("default").unwrap(),
2188 profile: None,
2189 origin: BindingOrigin::SyntheticEnvDefault,
2190 };
2191 assert!(synthetic.is_env_default());
2192
2193 let configured = AuthBindingRef {
2196 realm: RealmId::parse("env_default").unwrap(),
2197 binding: BindingId::parse("default").unwrap(),
2198 profile: None,
2199 origin: BindingOrigin::Configured,
2200 };
2201 assert!(!configured.is_env_default());
2202
2203 let s = serde_json::to_string(&synthetic).unwrap();
2205 assert!(s.contains("\"origin\":\"synthetic_env_default\""));
2206 let back: AuthBindingRef = serde_json::from_str(&s).unwrap();
2207 assert_eq!(back, synthetic);
2208
2209 let legacy = r#"{"realm":"env_default","binding":"default"}"#;
2211 let back: AuthBindingRef = serde_json::from_str(legacy).unwrap();
2212 assert_eq!(back.origin, BindingOrigin::Configured);
2213 assert!(!back.is_env_default());
2214 }
2215
2216 #[test]
2217 fn auth_binding_profile_overrides_binding_auth_profile() {
2218 let toml = r#"
2219realm_id = "prod"
2220default_binding = "primary"
2221
2222[backend.openai_default]
2223provider = "openai"
2224backend_kind = "openai_api"
2225base_url = "https://api.openai.com/v1"
2226
2227[auth.default_profile]
2228provider = "openai"
2229auth_method = "api_key"
2230source = { kind = "env", env = "OPENAI_API_KEY" }
2231
2232[auth.override_profile]
2233provider = "openai"
2234auth_method = "api_key"
2235source = { kind = "env", env = "OVERRIDE_OPENAI_API_KEY" }
2236
2237[binding.primary]
2238backend_profile = "openai_default"
2239auth_profile = "default_profile"
2240"#;
2241 let section: RealmConfigSection = toml::from_str(toml).unwrap();
2242 let realm = RealmConnectionSet::from_config("prod", §ion).unwrap();
2243 let auth_binding = AuthBindingRef {
2244 realm: RealmId::parse("prod").unwrap(),
2245 binding: BindingId::parse("primary").unwrap(),
2246 profile: Some(ProfileId::parse("override_profile").unwrap()),
2247 origin: BindingOrigin::Configured,
2248 };
2249
2250 let (_binding, _backend, auth) = realm.lookup_auth_binding(&auth_binding).unwrap();
2251 assert_eq!(auth.id, "override_profile");
2252 }
2253
2254 #[test]
2255 fn identity_slugs_reject_invalid_characters() {
2256 assert!(RealmId::parse("").is_err());
2257 assert!(BindingId::parse("bad space").is_err());
2258 assert!(ProfileId::parse("bad:colon").is_err());
2259 assert!(RealmId::parse("dev").is_ok());
2260 assert!(BindingId::parse("openai_default.v1").is_ok());
2261 }
2262
2263 #[test]
2264 fn credential_source_spec_serde() {
2265 for src in [
2266 CredentialSourceSpec::InlineSecret {
2267 secret: "sk-x".into(),
2268 },
2269 CredentialSourceSpec::ManagedStore,
2270 CredentialSourceSpec::Env {
2271 env: "OPENAI_API_KEY".into(),
2272 fallback: Vec::new(),
2273 },
2274 CredentialSourceSpec::ExternalResolver {
2275 handle: "desktop".into(),
2276 },
2277 CredentialSourceSpec::PlatformDefault,
2278 ] {
2279 let s = serde_json::to_string(&src).unwrap();
2280 let back: CredentialSourceSpec = serde_json::from_str(&s).unwrap();
2281 assert_eq!(back, src);
2282 }
2283 }
2284
2285 #[test]
2286 fn credential_source_spec_rejects_unknown_kind() {
2287 let bad = r#"{"kind":"nonexistent","foo":"bar"}"#;
2288 let err = serde_json::from_str::<CredentialSourceSpec>(bad).unwrap_err();
2289 assert!(
2290 err.to_string().contains("nonexistent") || err.to_string().contains("unknown variant"),
2291 "serde error should mention unknown variant: {err}",
2292 );
2293 }
2294
2295 #[test]
2296 fn env_default_openai_uses_public_openai_without_azure_envelope() {
2297 let realm = RealmConnectionSet::synthesize_env_default_from_lookup(
2298 Provider::OpenAI,
2299 lookup_from_pairs(&[]),
2300 );
2301 let backend = realm.backends.get("default").unwrap();
2302 let auth = realm.auth_profiles.get("default").unwrap();
2303
2304 assert_eq!(backend.backend_kind, "openai_api");
2305 assert_eq!(backend.base_url, None);
2306 assert_eq!(auth.auth_method, "api_key");
2307 assert_eq!(
2308 auth.source,
2309 CredentialSourceSpec::Env {
2310 env: "OPENAI_API_KEY".to_string(),
2311 fallback: Vec::new(),
2312 }
2313 );
2314 }
2315
2316 #[test]
2317 fn env_default_openai_uses_azure_when_key_and_endpoint_are_present() {
2318 let realm = RealmConnectionSet::synthesize_env_default_from_lookup(
2319 Provider::OpenAI,
2320 lookup_from_pairs(&[
2321 ("AZURE_OPENAI_API_KEY", "azure-key"),
2322 ("AZURE_OPENAI_ENDPOINT", "https://example.openai.azure.com/"),
2323 (
2324 "AZURE_OPENAI_IMAGE_GENERATION_DEPLOYMENT",
2325 "image-deployment-a",
2326 ),
2327 ("AZURE_OPENAI_IMAGE_GENERATION_API_VERSION", "preview"),
2328 ]),
2329 );
2330 let backend = realm.backends.get("default").unwrap();
2331 let auth = realm.auth_profiles.get("default").unwrap();
2332
2333 assert_eq!(backend.backend_kind, "azure_openai");
2334 assert_eq!(
2335 backend.base_url.as_deref(),
2336 Some("https://example.openai.azure.com/")
2337 );
2338 assert_eq!(
2339 backend.options["image_generation_deployment"],
2340 "image-deployment-a"
2341 );
2342 assert_eq!(backend.options["image_generation_api_version"], "preview");
2343 assert_eq!(auth.auth_method, "azure_api_key");
2344 assert_eq!(
2345 auth.source,
2346 CredentialSourceSpec::Env {
2347 env: "AZURE_OPENAI_API_KEY".to_string(),
2348 fallback: Vec::new(),
2349 }
2350 );
2351 }
2352
2353 #[test]
2354 fn env_default_openai_keeps_public_key_when_plain_azure_and_public_keys_are_both_set() {
2355 let realm = RealmConnectionSet::synthesize_env_default_from_lookup(
2356 Provider::OpenAI,
2357 lookup_from_pairs(&[
2358 ("OPENAI_API_KEY", "public-key"),
2359 ("AZURE_OPENAI_API_KEY", "azure-key"),
2360 ("AZURE_OPENAI_ENDPOINT", "https://example.openai.azure.com"),
2361 ]),
2362 );
2363 let backend = realm.backends.get("default").unwrap();
2364
2365 assert_eq!(backend.backend_kind, "openai_api");
2366 assert_eq!(backend.base_url, None);
2367 }
2368
2369 #[test]
2370 fn env_default_openai_rkat_azure_envelope_overrides_public_openai_key() {
2371 let realm = RealmConnectionSet::synthesize_env_default_from_lookup(
2372 Provider::OpenAI,
2373 lookup_from_pairs(&[
2374 ("OPENAI_API_KEY", "public-key"),
2375 ("RKAT_AZURE_OPENAI_API_KEY", "azure-key"),
2376 (
2377 "RKAT_AZURE_OPENAI_ENDPOINT",
2378 "https://example.openai.azure.com",
2379 ),
2380 ]),
2381 );
2382 let backend = realm.backends.get("default").unwrap();
2383 let auth = realm.auth_profiles.get("default").unwrap();
2384
2385 assert_eq!(backend.backend_kind, "azure_openai");
2386 assert_eq!(
2387 backend.base_url.as_deref(),
2388 Some("https://example.openai.azure.com")
2389 );
2390 assert_eq!(auth.auth_method, "azure_api_key");
2391 }
2392
2393 #[test]
2394 fn from_config_empty_section_yields_empty_set() {
2395 let section = RealmConfigSection::default();
2396 let set = RealmConnectionSet::from_config("dev", §ion).expect("empty section is valid");
2397 assert_eq!(set.realm_id.as_str(), "dev");
2398 assert!(set.backends.is_empty());
2399 assert!(set.auth_profiles.is_empty());
2400 assert!(set.bindings.is_empty());
2401 assert_eq!(set.default_binding, None);
2402 }
2403
2404 #[test]
2405 fn lookup_binding_returns_unknown_binding() {
2406 let set = RealmConnectionSet::from_config("dev", &RealmConfigSection::default())
2407 .expect("empty section valid");
2408 let err = set
2409 .lookup_binding("missing")
2410 .expect_err("empty set has no bindings");
2411 assert_eq!(err, ProviderBindingError::UnknownBinding("missing".into()));
2412 }
2413
2414 #[test]
2415 fn connection_target_uses_configured_realm_default_binding() {
2416 let config = openai_target_config();
2417 let preferred_realm = RealmId::parse("prod").unwrap();
2418 let target = resolve_realm_binding_target_for_provider(
2419 &config,
2420 Provider::OpenAI,
2421 None,
2422 None,
2423 None,
2424 Some(&preferred_realm),
2425 false,
2426 )
2427 .unwrap();
2428
2429 assert_eq!(target.auth_binding.realm.as_str(), "prod");
2430 assert_eq!(target.auth_binding.binding.as_str(), "primary");
2431 assert_eq!(target.binding.id, "primary");
2432 }
2433
2434 #[test]
2435 fn connection_target_explicit_binding_wins_with_preferred_realm() {
2436 let config = openai_target_config();
2437 let preferred_realm = RealmId::parse("prod").unwrap();
2438 let binding = BindingId::parse("secondary").unwrap();
2439 let target = resolve_realm_binding_target_for_provider(
2440 &config,
2441 Provider::OpenAI,
2442 None,
2443 Some(&binding),
2444 None,
2445 Some(&preferred_realm),
2446 false,
2447 )
2448 .unwrap();
2449
2450 assert_eq!(target.auth_binding.realm.as_str(), "prod");
2451 assert_eq!(target.auth_binding.binding.as_str(), "secondary");
2452 assert_eq!(target.binding.id, "secondary");
2453 }
2454
2455 #[test]
2460 fn connection_target_rejects_provider_mismatch() {
2461 let config = openai_target_config();
2462 let preferred_realm = RealmId::parse("prod").unwrap();
2463 let binding = BindingId::parse("primary").unwrap();
2464 let err = resolve_realm_binding_target_for_provider(
2465 &config,
2466 Provider::Anthropic,
2467 None,
2468 Some(&binding),
2469 None,
2470 Some(&preferred_realm),
2471 false,
2472 )
2473 .unwrap_err();
2474
2475 assert!(matches!(
2476 err,
2477 ConnectionTargetError::ProviderMismatch {
2478 expected: Provider::Anthropic,
2479 backend: Provider::OpenAI,
2480 auth: Provider::OpenAI,
2481 ..
2482 }
2483 ));
2484 }
2485
2486 fn openai_inherit_config(extra_child: &str) -> Config {
2490 config_with_realms(&format!(
2491 r#"
2492[global]
2493default_binding = "primary"
2494
2495[global.backend.openai_default]
2496provider = "openai"
2497backend_kind = "openai_api"
2498
2499[global.auth.openai_key]
2500provider = "openai"
2501auth_method = "chatgpt_oauth"
2502source = {{ kind = "platform_default" }}
2503
2504[global.binding.primary]
2505backend_profile = "openai_default"
2506auth_profile = "openai_key"
2507
2508[child]
2509parent = "global"
2510{extra_child}
2511"#
2512 ))
2513 }
2514
2515 #[test]
2517 fn inherited_binding_stamps_owning_realm_not_consuming_realm() {
2518 let cfg = openai_inherit_config("");
2519 let child = rid("child");
2520 let candidates = resolve_auth_binding_candidates_for_provider(
2521 &cfg,
2522 Provider::OpenAI,
2523 None,
2524 Some(&child),
2525 false,
2526 )
2527 .expect("resolve");
2528 assert_eq!(
2529 candidates[0].auth_binding.realm.as_str(),
2530 "global",
2531 "owner is the defining realm, not the consuming child"
2532 );
2533 assert_eq!(candidates[0].auth_binding.binding.as_str(), "primary");
2534 }
2535
2536 #[test]
2538 fn resolved_target_realm_equals_owning_connection_set_realm_id() {
2539 let cfg = openai_inherit_config("");
2540 let child = rid("child");
2541 let candidates = resolve_auth_binding_candidates_for_provider(
2542 &cfg,
2543 Provider::OpenAI,
2544 None,
2545 Some(&child),
2546 false,
2547 )
2548 .expect("resolve");
2549 let target = &candidates[0];
2550 assert_eq!(
2553 target.realm.realm_id.as_str(),
2554 target.auth_binding.realm.as_str()
2555 );
2556 assert_eq!(target.realm.realm_id.as_str(), "global");
2557 }
2558
2559 #[test]
2561 fn inherited_binding_resolves_backend_auth_in_owning_realm_only() {
2562 let cfg = openai_inherit_config(
2566 r#"
2567[child.auth.openai_key]
2568provider = "gemini"
2569auth_method = "api_key"
2570source = { kind = "platform_default" }
2571"#,
2572 );
2573 let child = rid("child");
2574 let target = resolve_auth_binding_or_default_for_provider(
2575 &cfg,
2576 Provider::OpenAI,
2577 None,
2578 Some(&child),
2579 false,
2580 )
2581 .expect("resolve");
2582 assert_eq!(target.auth_binding.realm.as_str(), "global");
2583 assert_eq!(
2584 target.auth_profile.provider,
2585 Provider::OpenAI,
2586 "auth must resolve in the owning (global) section, not child's shadow"
2587 );
2588 }
2589
2590 #[test]
2592 fn single_target_path_uses_unified_selection_policy() {
2593 let cfg = config_with_realms(
2596 r#"
2597[solo]
2598
2599[solo.backend.openai_default]
2600provider = "openai"
2601backend_kind = "openai_api"
2602
2603[solo.auth.openai_key]
2604provider = "openai"
2605auth_method = "chatgpt_oauth"
2606source = { kind = "platform_default" }
2607
2608[solo.binding.only]
2609backend_profile = "openai_default"
2610auth_profile = "openai_key"
2611"#,
2612 );
2613 let solo = rid("solo");
2614 let target = resolve_realm_binding_target_for_provider(
2615 &cfg,
2616 Provider::OpenAI,
2617 None,
2618 None,
2619 None,
2620 Some(&solo),
2621 false,
2622 )
2623 .expect("single unambiguous provider binding resolves without default_binding");
2624 assert_eq!(target.auth_binding.binding.as_str(), "only");
2625 assert_eq!(target.auth_binding.realm.as_str(), "solo");
2626 }
2627
2628 #[test]
2630 fn explicit_inherited_binding_resolves_at_owning_realm() {
2631 let cfg = openai_inherit_config("");
2632 let explicit = AuthBindingRef {
2633 realm: rid("child"),
2634 binding: BindingId::parse("primary").unwrap(),
2635 profile: None,
2636 origin: BindingOrigin::Configured,
2637 };
2638 let target = resolve_auth_binding_or_default_for_provider(
2639 &cfg,
2640 Provider::OpenAI,
2641 Some(&explicit),
2642 None,
2643 false,
2644 )
2645 .expect("explicit inherited binding resolves");
2646 assert_eq!(target.auth_binding.realm.as_str(), "global");
2647 assert_eq!(target.auth_binding.binding.as_str(), "primary");
2648 }
2649
2650 #[test]
2652 fn default_realm_literal_not_consulted_then_works_under_global() {
2653 let with_default = config_with_realms(
2655 r#"
2656[default]
2657default_binding = "p"
2658
2659[default.backend.openai_default]
2660provider = "openai"
2661backend_kind = "openai_api"
2662
2663[default.auth.openai_key]
2664provider = "openai"
2665auth_method = "chatgpt_oauth"
2666source = { kind = "platform_default" }
2667
2668[default.binding.p]
2669backend_profile = "openai_default"
2670auth_profile = "openai_key"
2671
2672[prod]
2673"#,
2674 );
2675 let prod = rid("prod");
2676 let candidates = resolve_auth_binding_candidates_for_provider(
2677 &with_default,
2678 Provider::OpenAI,
2679 None,
2680 Some(&prod),
2681 false,
2682 )
2683 .unwrap_or_default();
2684 assert!(
2685 candidates
2686 .iter()
2687 .all(|c| c.auth_binding.realm.as_str() != "default"),
2688 "the literal 'default' realm must not be consulted for an unrelated head"
2689 );
2690
2691 let under_global = config_with_realms(
2693 r#"
2694[global]
2695default_binding = "p"
2696
2697[global.backend.openai_default]
2698provider = "openai"
2699backend_kind = "openai_api"
2700
2701[global.auth.openai_key]
2702provider = "openai"
2703auth_method = "chatgpt_oauth"
2704source = { kind = "platform_default" }
2705
2706[global.binding.p]
2707backend_profile = "openai_default"
2708auth_profile = "openai_key"
2709
2710[prod]
2711"#,
2712 );
2713 let candidates = resolve_auth_binding_candidates_for_provider(
2714 &under_global,
2715 Provider::OpenAI,
2716 None,
2717 Some(&prod),
2718 false,
2719 )
2720 .expect("global is inherited via the implicit tail");
2721 assert_eq!(candidates[0].auth_binding.realm.as_str(), "global");
2722 assert_eq!(candidates[0].auth_binding.binding.as_str(), "p");
2723 }
2724
2725 #[test]
2727 fn nearest_child_wins_when_head_and_ancestor_both_define_binding() {
2728 let cfg = config_with_realms(
2729 r#"
2730[global]
2731default_binding = "g"
2732
2733[global.backend.openai_default]
2734provider = "openai"
2735backend_kind = "openai_api"
2736
2737[global.auth.openai_key]
2738provider = "openai"
2739auth_method = "chatgpt_oauth"
2740source = { kind = "platform_default" }
2741
2742[global.binding.g]
2743backend_profile = "openai_default"
2744auth_profile = "openai_key"
2745
2746[team]
2747parent = "global"
2748default_binding = "t"
2749
2750[team.backend.openai_default]
2751provider = "openai"
2752backend_kind = "openai_api"
2753
2754[team.auth.openai_key]
2755provider = "openai"
2756auth_method = "chatgpt_oauth"
2757source = { kind = "platform_default" }
2758
2759[team.binding.t]
2760backend_profile = "openai_default"
2761auth_profile = "openai_key"
2762"#,
2763 );
2764 let team = rid("team");
2765 let candidates = resolve_auth_binding_candidates_for_provider(
2766 &cfg,
2767 Provider::OpenAI,
2768 None,
2769 Some(&team),
2770 false,
2771 )
2772 .expect("resolve");
2773 assert_eq!(
2774 candidates[0].auth_binding.realm.as_str(),
2775 "team",
2776 "nearest-child first"
2777 );
2778 assert_eq!(candidates[0].auth_binding.binding.as_str(), "t");
2779 assert_eq!(candidates[1].auth_binding.realm.as_str(), "global");
2780 assert_eq!(candidates[1].auth_binding.binding.as_str(), "g");
2781 }
2782
2783 #[test]
2785 fn explicit_env_default_ref_rejected_before_chain_walk() {
2786 let cfg = openai_inherit_config("");
2787 let env_ref = AuthBindingRef {
2788 realm: RealmId::from_known_valid(ENV_DEFAULT_REALM_SLUG),
2789 binding: BindingId::parse("default").unwrap(),
2790 profile: None,
2791 origin: BindingOrigin::SyntheticEnvDefault,
2792 };
2793 let err = resolve_auth_binding_or_default_for_provider(
2794 &cfg,
2795 Provider::OpenAI,
2796 Some(&env_ref),
2797 None,
2798 true,
2799 )
2800 .unwrap_err();
2801 assert!(matches!(err, ConnectionTargetError::UnknownRealm(_)));
2802 }
2803
2804 #[test]
2806 fn resolve_write_owner_classifies_owned_inherited_and_unknown() {
2807 let cfg = openai_inherit_config(""); let primary = BindingId::parse("primary").unwrap();
2809
2810 assert_eq!(
2812 resolve_write_owner(&cfg, &rid("global"), &primary)
2813 .expect("global owns primary")
2814 .as_str(),
2815 "global"
2816 );
2817
2818 let err = resolve_write_owner(&cfg, &rid("child"), &primary).unwrap_err();
2820 assert!(
2821 matches!(&err, WriteOwnerError::Inherited { owner, .. } if owner == "global"),
2822 "expected Inherited{{owner=global}}, got {err:?}"
2823 );
2824
2825 let err = resolve_write_owner(&cfg, &rid("child"), &BindingId::parse("nope").unwrap())
2827 .unwrap_err();
2828 assert!(matches!(err, WriteOwnerError::Unknown { .. }));
2829 }
2830
2831 #[test]
2832 fn auth_binding_candidates_prefer_provider_binding_in_preferred_realm() {
2833 let config = config_with_realms(
2834 r#"
2835[dev]
2836default_binding = "openai_oauth"
2837
2838[dev.backend.openai_chatgpt]
2839provider = "openai"
2840backend_kind = "openai_chatgpt"
2841
2842[dev.auth.openai_oauth]
2843provider = "openai"
2844auth_method = "chatgpt_oauth"
2845source = { kind = "managed_store" }
2846
2847[dev.binding.openai_oauth]
2848backend_profile = "openai_chatgpt"
2849auth_profile = "openai_oauth"
2850default_model = "test-openai-default"
2851"#,
2852 );
2853 let preferred_realm = RealmId::parse("dev").unwrap();
2854
2855 let candidates = resolve_auth_binding_candidates_for_provider(
2856 &config,
2857 Provider::OpenAI,
2858 None,
2859 Some(&preferred_realm),
2860 true,
2861 )
2862 .expect("candidates resolve");
2863
2864 assert_eq!(candidates[0].auth_binding.realm.as_str(), "dev");
2865 assert_eq!(candidates[0].auth_binding.binding.as_str(), "openai_oauth");
2866 assert!(!candidates[0].auth_binding.is_env_default());
2867 }
2868
2869 #[test]
2873 fn auth_binding_candidates_exclude_unrelated_sibling_realm() {
2874 let config = config_with_realms(
2875 r#"
2876[dev]
2877
2878[dev.backend.openai_chatgpt]
2879provider = "openai"
2880backend_kind = "openai_chatgpt"
2881
2882[dev.auth.openai_oauth]
2883provider = "openai"
2884auth_method = "chatgpt_oauth"
2885source = { kind = "managed_store" }
2886
2887[dev.binding.openai_oauth]
2888backend_profile = "openai_chatgpt"
2889auth_profile = "openai_oauth"
2890"#,
2891 );
2892 let preferred_realm = RealmId::parse("missing").unwrap();
2895
2896 let candidates = resolve_auth_binding_candidates_for_provider(
2897 &config,
2898 Provider::OpenAI,
2899 None,
2900 Some(&preferred_realm),
2901 true,
2902 )
2903 .expect("candidates resolve");
2904
2905 assert!(
2906 candidates
2907 .iter()
2908 .all(|c| c.auth_binding.realm.as_str() != "dev"),
2909 "unrelated sibling 'dev' must not be discovered via a flat scan"
2910 );
2911 assert_eq!(candidates.len(), 1);
2913 assert!(candidates[0].auth_binding.is_env_default());
2914 assert_eq!(
2915 candidates[0].auth_binding.origin,
2916 BindingOrigin::SyntheticEnvDefault
2917 );
2918 }
2919
2920 #[test]
2921 fn from_inline_api_keys_marks_each_provider_default() {
2922 let section = RealmConfigSection::from_inline_api_keys(&[
2923 ("anthropic", "sk-ant"),
2924 ("openai", "sk-oai"),
2925 ]);
2926 assert!(section.binding["default_anthropic"].provider_default);
2929 assert!(section.binding["default_openai"].provider_default);
2930 assert_eq!(
2932 section.default_binding.as_deref(),
2933 Some("default_anthropic")
2934 );
2935 }
2936
2937 #[test]
2938 fn selected_binding_prefers_typed_provider_default_marker() {
2939 let config = config_with_realms(
2942 r#"
2943[dev]
2944
2945[dev.backend.openai_default]
2946provider = "openai"
2947backend_kind = "openai_api"
2948
2949[dev.auth.openai_api]
2950provider = "openai"
2951auth_method = "api_key"
2952source = { kind = "env", env = "OPENAI_API_KEY" }
2953
2954[dev.binding.alpha]
2955backend_profile = "openai_default"
2956auth_profile = "openai_api"
2957
2958[dev.binding.beta]
2959backend_profile = "openai_default"
2960auth_profile = "openai_api"
2961provider_default = true
2962"#,
2963 );
2964 let preferred_realm = RealmId::parse("dev").unwrap();
2965 let candidates = resolve_auth_binding_candidates_for_provider(
2966 &config,
2967 Provider::OpenAI,
2968 None,
2969 Some(&preferred_realm),
2970 false,
2971 )
2972 .expect("candidates resolve");
2973
2974 assert_eq!(candidates[0].auth_binding.realm.as_str(), "dev");
2975 assert_eq!(candidates[0].auth_binding.binding.as_str(), "beta");
2976 }
2977
2978 #[test]
2979 fn realm_config_section_serde_empty() {
2980 let section = RealmConfigSection::default();
2981 let s = serde_json::to_string(§ion).unwrap();
2982 assert_eq!(s, "{}");
2984 }
2985
2986 #[test]
2987 fn realm_config_section_serde_populated() {
2988 let toml_input = r#"
2992default_binding = "default_openai"
2993
2994[backend.openai_default]
2995provider = "openai"
2996backend_kind = "openai_api"
2997base_url = "https://api.openai.com"
2998
2999[auth.openai_api_key]
3000provider = "openai"
3001auth_method = "api_key"
3002source = { kind = "env", env = "OPENAI_API_KEY" }
3003
3004[binding.default_openai]
3005backend_profile = "openai_default"
3006auth_profile = "openai_api_key"
3007default_model = "test-openai-other"
3008"#;
3009 let section: RealmConfigSection = toml::from_str(toml_input).unwrap();
3010 assert_eq!(section.backend.len(), 1);
3011 assert_eq!(section.auth.len(), 1);
3012 assert_eq!(section.binding.len(), 1);
3013 assert_eq!(section.default_binding.as_deref(), Some("default_openai"));
3014 assert_eq!(
3015 section.backend["openai_default"].base_url.as_deref(),
3016 Some("https://api.openai.com"),
3017 );
3018 }
3019}