Skip to main content

meerkat_core/
connection.rs

1//! Realm-scoped connection contracts: backend profiles, auth profiles,
2//! provider bindings, and the ingestion wrapper `RealmConfigSection`.
3//!
4//! This module owns the cross-cutting runtime shapes used by sessions,
5//! factories, and surfaces. Provider-runtime-side typed enums
6//! (`OpenAiBackendKind`, `AnthropicAuthMethod`, etc.) live in
7//! [`crate::provider_matrix`]. Runtime config still carries `backend_kind` /
8//! `auth_method` as strings until they are normalized at the provider-runtime
9//! catalog boundary.
10
11use 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// ---------------------------------------------------------------------
44// Runtime shapes (what providers/surfaces consume at runtime)
45// ---------------------------------------------------------------------
46
47/// Error returned when a realm/binding/profile slug fails validation.
48#[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
59/// `skip_serializing_if` helper: keeps `bool` fields off the wire when false,
60/// matching the crate convention (`tool_catalog::is_false`).
61fn 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            /// Construct from a compile-time-known-valid slug literal used
93            /// internally by synthesis helpers (e.g. the `"env_default"` /
94            /// `"default"` synthetic-fallback slugs). A `debug_assert`
95            /// validates the slug in debug builds; release builds skip the
96            /// check since the only callers pass static, already-valid slugs.
97            // Generated for every slug newtype; only some (e.g. RealmId) have a
98            // synthesis caller.
99            #[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
144/// The single owner of the synthetic env-var-default realm slug. The literal
145/// lives here once; `synthesize_env_default` mints it and [`RealmId::is_env_default`]
146/// recognizes it, so no other site recovers the "this is the env-var default
147/// realm" fact by comparing a raw `"env_default"` string.
148pub const ENV_DEFAULT_REALM_SLUG: &str = "env_default";
149
150/// The single owner of the reserved global-realm slug. The `global` realm is
151/// the durable root of the default inheritance chain: a realm with no explicit
152/// `parent` edge that is not itself `global` implicitly parents to it. Unlike
153/// [`ENV_DEFAULT_REALM_SLUG`] (the ephemeral synthetic env-var fallback),
154/// `global` is a normal `Configured` realm that may hold persisted credentials
155/// and publish durable leases. Recognized via [`RealmId::is_global`], never by
156/// raw string comparison elsewhere.
157pub const GLOBAL_REALM_SLUG: &str = "global";
158
159/// Hard cap on realm parent-chain length. A finite config is already bounded by
160/// the `seen` dedup set; this is a belt-and-suspenders guard that bounds work
161/// and stack independently of config size, and yields a typed error instead of
162/// looping. 16 is far beyond any plausible org→team→user→global nesting.
163pub const MAX_REALM_CHAIN_DEPTH: usize = 16;
164
165impl RealmId {
166    /// True when this realm is the synthetic env-var-default realm (the realm
167    /// [`RealmConnectionSet::synthesize_env_default`] mints). Routing/selection
168    /// decisions consult this typed predicate instead of a `== "env_default"`
169    /// slug comparison.
170    #[must_use]
171    pub fn is_env_default(&self) -> bool {
172        self.as_str() == ENV_DEFAULT_REALM_SLUG
173    }
174
175    /// True when this realm is the reserved `global` root of the inheritance
176    /// chain. Consulted via this typed predicate, never by a raw
177    /// `== "global"` comparison.
178    #[must_use]
179    pub fn is_global(&self) -> bool {
180        self.as_str() == GLOBAL_REALM_SLUG
181    }
182
183    /// Mint the reserved `global` [`RealmId`]. Infallible: the slug is a
184    /// compile-time-valid constant.
185    #[must_use]
186    pub fn global() -> RealmId {
187        RealmId::from_known_valid(GLOBAL_REALM_SLUG)
188    }
189}
190
191/// Origin discriminant for an [`AuthBindingRef`].
192///
193/// Distinguishes a binding that names a durable, config-resolvable identity
194/// (`Configured`) from the synthetic env-var fallback the resolver mints when
195/// no realm config exists but a well-known API-key env var is set
196/// (`SyntheticEnvDefault`). The synthetic origin is ephemeral: it must never be
197/// rehydrated as a durable identity nor publish a durable auth lease.
198///
199/// This is the typed owner of the "is this the env-var default?" fact, replacing
200/// the prior recovery-by-magic-slug (`realm == "env_default"`,
201/// `binding == "default"`). Identity slugs (`RealmId`/`BindingId`) are pure
202/// opaque identity again; origin is carried explicitly.
203#[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    /// A durable, config-resolvable binding. This is the back-read default so
208    /// that wire/persisted rows written before this field existed deserialize
209    /// as a configured identity.
210    #[default]
211    Configured,
212    /// The synthetic env-var fallback binding (ephemeral, not durable).
213    SyntheticEnvDefault,
214}
215
216/// Session-facing reference to a binding inside a realm.
217///
218/// `AuthBindingRef` is purely structural — it does NOT carry a `"realm:binding"`
219/// string form. Wave-b deleted `parse` and `Display` so that no code path
220/// accidentally ferries the opaque join through the runtime. CLI input that
221/// arrives as `"realm:binding[:profile]"` must be split at the CLI boundary
222/// and constructed field-by-field.
223#[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    /// Whether this ref names a configured durable identity or the synthetic
231    /// env-var fallback. Defaults to [`BindingOrigin::Configured`] for back-read
232    /// of rows persisted before the discriminant existed.
233    #[serde(default, skip_serializing_if = "BindingOrigin::is_configured")]
234    pub origin: BindingOrigin,
235}
236
237impl BindingOrigin {
238    /// True when this is the (default) configured origin. Used to keep the
239    /// serialized shape wire-additive via `skip_serializing_if`.
240    pub fn is_configured(&self) -> bool {
241        matches!(self, BindingOrigin::Configured)
242    }
243}
244
245impl AuthBindingRef {
246    /// True when this ref is the synthetic env-var fallback binding rather than
247    /// a durable configured identity. Reads the typed [`BindingOrigin`]
248    /// discriminant; no slug-string comparison.
249    pub fn is_env_default(&self) -> bool {
250        matches!(self.origin, BindingOrigin::SyntheticEnvDefault)
251    }
252}
253
254/// The realm identity a mob member binds to.
255///
256/// This is the single fail-closed owner of the `mob.{mob_id}` realm form.
257/// Both the producer (mob build) and the consumer (mob-mcp ownership routing)
258/// derive their realm string through this helper, so the dot/colon divergence
259/// that previously made `persisted_mob_binding` never match a real session is
260/// impossible: there is one form, validated once.
261pub fn mob_realm_id(mob_id: &str) -> Result<RealmId, IdentityError> {
262    RealmId::parse(format!("mob.{mob_id}"))
263}
264
265/// Error returned when a [`MemberCommsName`] fails to parse.
266#[derive(Debug, Clone, PartialEq, Eq, Error)]
267pub enum MemberCommsNameError {
268    /// The name did not have exactly three `/`-separated components.
269    #[error(
270        "mob member comms name must have exactly three '/'-separated components (mob_id/role/member)"
271    )]
272    WrongComponentCount,
273    /// A component was empty or contained characters outside the identifier-safe set.
274    #[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
281/// Validate one component of a [`MemberCommsName`].
282///
283/// Folds the former `is_valid_peer_name_component` rule (first char ASCII
284/// alphabetic or `_`; remaining chars ASCII alphanumeric / `-` / `_`). This is
285/// strictly tighter than [`validate_slug`], so any valid component is also a
286/// valid realm slug — which is why `mob.{component}` always parses.
287fn 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/// Typed mob-member comms (peer) name: `mob_id/role/member`.
308///
309/// This is the single owner of the `{mob_id}/{role}/{member}` join, with one
310/// [`Display`](std::fmt::Display) (render) and one fail-closed
311/// [`FromStr`](std::str::FromStr) (parse, exactly three identifier-safe
312/// components). It replaces the scattered `format!("{}/{}/{}", ..)` producers
313/// and the hand-rolled `split('/')` consumers, so the routing-name shape is no
314/// longer recovered by string convention.
315///
316/// Identity and transport are separate facts: this is the transport routing
317/// *name* (a [`crate::comms::PeerName`]). Durable identity ownership lives in
318/// [`MobMemberBinding`].
319#[derive(Debug, Clone, PartialEq, Eq, Hash)]
320pub struct MemberCommsName {
321    mob_id: String,
322    role: String,
323    member: String,
324}
325
326impl MemberCommsName {
327    /// Construct from already-typed components, validating each.
328    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    /// The durable identity binding implied by this comms name.
359    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/// Typed role of a peer relative to a mob.
387///
388/// Replaces the magic `"external"` string the synthetic peer-added fallback
389/// previously invented when a peer name failed to parse as a member comms
390/// name. A peer is either a `Member` of a mob (carrying its parsed role) or
391/// `External`.
392#[derive(Debug, Clone, PartialEq, Eq)]
393pub enum PeerRole {
394    /// A peer that is a member of a mob, with its parsed role component.
395    Member(String),
396    /// A peer that is not a recognized mob member.
397    External,
398}
399
400impl PeerRole {
401    /// The wire/display label for this role.
402    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/// Durable, typed identity of a mob member, carried on
411/// [`SessionMetadata`](crate::session::SessionMetadata).
412///
413/// This is the canonical owner of the `(mob_id, role, member)` identity fact
414/// that ownership routing (`owns_persisted_bridge_session`) and outbound
415/// peer-added payloads previously recovered by splitting the untyped
416/// `comms_name` string and re-deriving the realm by format convention.
417///
418/// `comms_name`/`realm_id`/`peer_meta` remain on the metadata as the transport
419/// routing name and discovery metadata — identity and transport are separate
420/// facts. Old persisted rows written before this field existed deserialize as
421/// `None` (the field is `#[serde(default, skip_serializing_if)]` on the
422/// metadata), so back-read is safe.
423#[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    /// The transport comms name implied by this binding.
434    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/// Backend profile: where requests go and which backend contract applies.
440#[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/// Auth profile: how credentials are obtained, refreshed, constrained.
453#[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/// Typed identity of an externally-registered auth resolver.
467///
468/// Resolver handles are free-form names chosen by the host at registration, so
469/// this is carried as a string on the wire; the newtype keeps it a distinct
470/// typed identity in memory (and as the resolver-registry map key) so it cannot
471/// be confused with any other arbitrary string.
472#[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/// Where credentials come from.
506#[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    /// Binding-scoped credential material stored in the configured
514    /// [`TokenStore`](crate::auth::TokenStore). The storage key is the
515    /// resolved typed binding identity (`realm`, `binding`), not a
516    /// second free-form profile string.
517    ManagedStore,
518    Env {
519        env: String,
520        /// Ordered fallback env var names consulted when `env` is
521        /// unset. Used for providers with multiple well-known names
522        /// (e.g. Gemini falls back to `GOOGLE_API_KEY` when
523        /// `GEMINI_API_KEY` is absent). The resolver's RKAT_*-prefix
524        /// precedence applies to each name in turn.
525        #[serde(default, skip_serializing_if = "Vec::is_empty")]
526        fallback: Vec<String>,
527    },
528    ExternalResolver {
529        handle: ExternalResolverId,
530    },
531    PlatformDefault,
532    /// External command that prints a bearer token on stdout. Reference:
533    /// Codex `external_bearer.rs:17-157`. The runner lives in
534    /// `meerkat-client/src/auth_store/command.rs`.
535    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        /// Timeout for the subprocess in milliseconds.
544        #[serde(default = "default_command_timeout_ms")]
545        timeout_ms: u64,
546        /// Optional cached-token lifetime. `None` disables caching.
547        #[serde(default, skip_serializing_if = "Option::is_none")]
548        refresh_interval_ms: Option<u64>,
549    },
550    /// Read credentials from an inherited file descriptor (Claude Code
551    /// pattern for sandboxed host-injected tokens).
552    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/// Policy overrides carried on a binding.
588#[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/// A binding is what sessions actually refer to: one backend + one auth
600/// profile, plus policy and an optional default model.
601#[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    /// Typed per-binding marker: this binding is the default for its provider.
612    /// Owns the "default for provider X" fact that was previously carried only
613    /// by the `default_<provider>` name convention. The realm-level
614    /// [`RealmConnectionSet::default_binding`] expresses a single per-realm
615    /// default; this flag expresses the per-provider default.
616    #[serde(default, skip_serializing_if = "is_false")]
617    pub provider_default: bool,
618}
619
620/// Realm-scoped set of backends, auth profiles, and bindings.
621///
622/// Produced by [`RealmConnectionSet::from_config`] from a
623/// [`RealmConfigSection`] ingested from TOML.
624#[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/// Fully resolved connection target selected from config-owned identity
636/// policy. Surfaces should use this instead of inventing realm or binding
637/// defaults locally.
638#[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/// Fail-closed errors from resolving a realm parent chain.
691///
692/// The chain walk is fully typed and panic-free: every malformed topology
693/// yields one of these variants rather than looping, unwrapping, or silently
694/// truncating. Internal to resolution (not a wire type).
695#[derive(Debug, Clone, Error, PartialEq, Eq)]
696pub enum RealmChainError {
697    /// A `parent` edge re-enters an already-visited realm (includes a realm
698    /// naming itself as parent). The captured path is for diagnostics.
699    #[error("realm parent chain has a cycle: {}", .chain.join(" -> "))]
700    Cycle { chain: Vec<String> },
701    /// The chain exceeded [`MAX_REALM_CHAIN_DEPTH`].
702    #[error("realm parent chain from '{head}' exceeds max depth {max}")]
703    DepthExceeded { head: String, max: usize },
704    /// A `parent` edge names a realm absent from config (and it is not the
705    /// reserved `global` root, which is allowed to be implicit).
706    #[error("realm '{realm}' names parent '{parent}' which is not configured")]
707    MissingParent { realm: String, parent: String },
708    /// The reserved `global` realm declares a `parent`; it must be the root.
709    #[error("the reserved 'global' realm (via '{realm}') must not declare a parent")]
710    GlobalHasParent { realm: String },
711    /// A `parent` edge targets the synthetic env-var-default slug, which may
712    /// never be a chain node.
713    #[error("realm '{realm}' names the reserved env_default slug as its parent")]
714    ParentIsEnvDefault { realm: String },
715}
716
717/// An ordered realm inheritance chain, most-derived first.
718///
719/// `realms()[0]` is the head (consuming) realm; the last element is the
720/// reserved `global` root when one participates. Built only via the fallible
721/// [`RealmChain::resolve`]; the field is private so the only way to obtain a
722/// chain is through the validated walk.
723#[derive(Debug, Clone, PartialEq, Eq)]
724pub struct RealmChain {
725    realms: Vec<RealmId>,
726}
727
728impl RealmChain {
729    /// Resolve the parent chain for `head`, walking `parent` edges to the root.
730    ///
731    /// Ordering is `[head, parent, .., global?]` — fully determined by the
732    /// linear `parent` edges, with zero dependence on map iteration order. A
733    /// realm with no explicit `parent` that is not itself `global` implicitly
734    /// appends `global` IFF `global` is configured and not already visited.
735    ///
736    /// Absent head: if `head` is not in `config.realm`, the chain is `[head]`
737    /// alone (it contributes no section) plus the implicit-global tail when
738    /// applicable — it is NOT a hard error. Callers that require an explicit
739    /// realm to exist enforce that separately (see the explicit-ref path in
740    /// the connection resolvers). Fails closed on cycle, depth, missing
741    /// parent, a parent pointing at `global`-with-a-parent, or a parent that
742    /// is the env_default slug. Iterative (no recursion) — wasm stack-safe.
743    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        // Seed with the head so a realm naming itself as parent is a 1-cycle.
748        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            // A `global` node must be the root: it may not declare a parent.
761            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                // No explicit parent: terminate. Append the implicit `global`
770                // tail when the current terminal is not already `global`, the
771                // global realm is configured, and it has not been visited.
772                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            // A parent edge must resolve to a configured realm, except the
787            // reserved `global` root which is allowed to be implicit/absent.
788            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    /// The resolved chain, most-derived (head) first, root (`global`) last.
806    #[must_use]
807    pub fn realms(&self) -> &[RealmId] {
808        &self.realms
809    }
810}
811
812/// Synthesize the typed env-var-default target for `provider`.
813///
814/// The single owner of the synthetic fallback materialization, shared by the
815/// single-target and candidate resolvers so the ephemeral
816/// [`BindingOrigin::SyntheticEnvDefault`] identity is minted in exactly one
817/// place.
818fn 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
837/// Walk `head`'s realm parent chain and collect, in chain order, one
838/// owner-stamped provider candidate per chain member that defines a usable
839/// provider binding (via the unified [`selected_binding_id_for_provider`]
840/// policy). This is the single owner of the default-selection cross-realm
841/// candidate order — it replaces both the deleted flat scan over
842/// `config.realm.keys()` and the deleted literal `"default"` realm candidate.
843///
844/// Provenance (decision A): each candidate is materialized from the OWNING
845/// chain member's OWN [`RealmConnectionSet`], so `AuthBindingRef.realm` is the
846/// realm that DEFINES the binding, and `materialize_connection_target` stamps
847/// `realm.realm_id` for an owner whose `realm_id == owner` by construction —
848/// the strict registry equality stays a real invariant with no relaxation.
849///
850/// Isolation (per-member fail-closed, MF-03): an ancestor member with an
851/// absent or structurally-invalid section is SKIPPED (it cannot take down
852/// resolution for valid descendants). The head isolates the same way unless
853/// `head_required`, in which case an absent head is `UnknownRealm` and an
854/// invalid head is `RealmConfigInvalid`.
855fn 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
907/// Resolve an EXPLICIT binding id along `head`'s chain, returning the
908/// owner-stamped target for the first chain member (child-first) that defines
909/// it. A binding may be inherited: the head names the consuming realm, but the
910/// owner stamped is the chain member that actually declares the binding.
911///
912/// Head validity: an absent head is `UnknownRealm` and an invalid head is
913/// `RealmConfigInvalid` when `head_required`; ancestors isolate. If no chain
914/// member declares the binding, fail closed with `BindingInvalid`/UnknownBinding
915/// attributed to the head realm. A provider mismatch on the explicitly named
916/// binding propagates as `ProviderMismatch` (explicit requests are strict).
917fn 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
965/// Resolve an explicitly named auth binding and infer its provider from the
966/// effective realm configuration.
967///
968/// This is the provider-neutral companion to
969/// [`resolve_auth_binding_or_default_for_provider`]. It exists for ingress
970/// seams where a caller supplied `auth_binding` but omitted `provider`: the
971/// binding's configured backend/auth pair is the typed provider authority.
972/// Resolution walks the named realm's inheritance chain and returns the
973/// owner-stamped binding; callers must not reimplement that walk or infer a
974/// provider from binding names.
975pub 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        /* head_required = */ true,
991    )
992}
993
994/// Resolve a connection target from config-owned identity facts.
995///
996/// `explicit_realm` / `explicit_binding` are request atoms, not defaults.
997/// When either is absent, selection falls back to the preferred realm and
998/// that realm's `default_binding`, then to the configured `default` realm.
999/// Provider-shaped binding names and hard-coded realm names must not be
1000/// encoded by REST/RPC/SDK surfaces.
1001pub 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    // Head of the chain: an explicit realm names it (and must exist); else the
1011    // preferred realm; else the reserved `global` root. Resolution walks
1012    // head -> parents -> global; an unrelated sibling realm is NOT a candidate
1013    // (the flat scan and the literal `default` realm are both gone — `global`
1014    // is the universal default head).
1015    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
1046/// Resolve an explicit [`AuthBindingRef`] or the configured default target for
1047/// the selected provider. This is the shared factory/runtime path for
1048/// auth-binding-less provider resolution.
1049pub 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        // The synthetic env-var fallback is never a durable, config-resolvable
1058        // identity: reject it BEFORE any chain walk (guards the reorder).
1059        if auth_binding.is_env_default() {
1060            return Err(ConnectionTargetError::UnknownRealm(
1061                auth_binding.realm.as_str().to_string(),
1062            ));
1063        }
1064        // Walk the named realm's chain so an explicitly-referenced binding that
1065        // is defined only in a parent/global resolves at its OWNING realm.
1066        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            /* head_required = */ 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            // Typed per-provider default marker replaces the
1113            // `default_<provider>` name convention. First marked wins
1114            // (BTreeMap iteration is deterministic by id).
1115            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
1152/// Resolve ordered connection candidates for an omitted `auth_binding`.
1153///
1154/// The returned order is the shared "best available" policy used by all
1155/// factory-backed surfaces, now driven by the realm parent chain:
1156/// 1. provider binding in the preferred (head) realm
1157/// 2. provider binding in each ancestor realm, child-first
1158/// 3. provider binding in the reserved `global` root (implicit chain tail)
1159/// 4. synthetic env-var fallback when allowed
1160///
1161/// An unrelated sibling realm is NOT a candidate — the prior flat scan over
1162/// `config.realm.keys()` and the literal `"default"` realm candidate are both
1163/// removed; shared credentials belong in `[realm.global]` (inherited by every
1164/// realm via the implicit tail) or a named parent. Within a realm,
1165/// `default_binding` wins when it resolves to the requested provider, then the
1166/// typed `provider_default` marker, then a single unambiguous provider binding.
1167/// Explicit `auth_binding` still resolves to one strict (owner-stamped) target.
1168pub 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    // Head defaults to the reserved `global` root when no realm is preferred,
1187    // so an unscoped lookup still resolves the universal default. Candidate
1188    // discovery never requires the head to exist (an unmaterialized session
1189    // realm still inherits its chain / global); ancestors isolate.
1190    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/// Outcome of classifying where a credential WRITE may land for `(head, binding)`.
1208///
1209/// Strict-owner write (decision 5): credential reads inherit down the chain, but
1210/// a write may target only the realm that DEFINES the binding in its own
1211/// section. This is the SINGLE owner of that policy — surfaces (REST/RPC/CLI)
1212/// call it and merely map the typed error to their transport status, rather
1213/// than each re-deriving "is this binding inherited?".
1214#[derive(Debug, Clone, Error, PartialEq, Eq)]
1215pub enum WriteOwnerError {
1216    /// The binding is inherited from an ancestor realm; the write must target
1217    /// the owning realm, not the consuming `head`.
1218    #[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    /// No realm on `head`'s chain defines the binding.
1229    #[error("binding '{binding}' is not defined on realm '{head}' or any realm it inherits from")]
1230    Unknown { binding: String, head: String },
1231    /// The realm parent chain could not be resolved.
1232    #[error(transparent)]
1233    Chain(#[from] RealmChainError),
1234}
1235
1236/// Classify the owning realm for a credential WRITE to `(head, binding)`.
1237///
1238/// Returns the head itself when it defines the binding in its OWN section
1239/// (write allowed). Returns [`WriteOwnerError::Inherited`] naming the owning
1240/// ancestor when the binding is only inherited (write rejected, strict-owner),
1241/// or [`WriteOwnerError::Unknown`] when no chain member defines it.
1242pub 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    // Skip the head (already checked); the first ancestor that defines it owns it.
1258    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    // `realm.realm_id` is already a typed `RealmId` (parsed once at
1279    // `from_config`/synthesis); no re-parse needed.
1280    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    /// Validate and materialize a realm connection set from its config
1318    /// section. Normalizes provider strings into the typed
1319    /// [`Provider`] enum and verifies that every binding references
1320    /// existing backend and auth profiles whose providers agree.
1321    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 &section.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            // id uniqueness within a single BTreeMap key space is
1342            // guaranteed by the map itself; no extra check needed.
1343            backends.insert(id.clone(), backend);
1344        }
1345
1346        let mut auth_profiles: BTreeMap<String, AuthProfile> = BTreeMap::new();
1347        for (id, cfg) in &section.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 &section.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    /// Synthesize a default [`RealmConnectionSet`] for a given provider,
1397    /// sourcing credentials from a well-known env var. Used by surface
1398    /// factories when no explicit realm config exists but the user has
1399    /// set `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GEMINI_API_KEY` in
1400    /// the environment. OpenAI also supports an Azure env envelope:
1401    /// `AZURE_OPENAI_API_KEY` plus `AZURE_OPENAI_ENDPOINT` synthesizes the
1402    /// `azure_openai` backend instead of public OpenAI when no public OpenAI
1403    /// key is present. The synthesized realm is consumed by the same
1404    /// `ProviderRuntimeRegistry` path as explicit realms, so env-var auth and
1405    /// realm-config auth share one resolution pipeline.
1406    ///
1407    /// Returns a realm with id `"env_default"` containing one binding
1408    /// `"default"` pointing at:
1409    /// - BackendProfile `"default"` with the provider's default
1410    ///   backend_kind and base_url=None (provider client uses its default).
1411    /// - AuthProfile `"default"` with `source = Env { env: <ENV_VAR> }` and
1412    ///   the provider-specific env auth method.
1413    ///
1414    /// The ENV_VAR name is per-provider:
1415    /// - Anthropic: `ANTHROPIC_API_KEY`
1416    /// - OpenAI:   `OPENAI_API_KEY`
1417    /// - Azure OpenAI: `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_ENDPOINT`
1418    /// - Google:   `GEMINI_API_KEY`
1419    ///
1420    /// Callers should also honor `RKAT_*`-prefixed overrides via
1421    /// `ResolverEnvironment::with_process_env()`; that lookup is applied
1422    /// inside the registry's resolve path when it reads the env source.
1423    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    /// Testable variant of [`Self::synthesize_env_default`] that lets callers
1428    /// inject the env lookup used to select the OpenAI public-vs-Azure default.
1429    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    /// Resolve a binding by id. Returns the binding plus its referenced
1481    /// backend and auth profiles.
1482    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    /// Resolve a typed auth binding reference. `AuthBindingRef.profile`, when
1502    /// present, overrides the binding's configured auth profile while keeping
1503    /// the binding's backend and policy authoritative.
1504    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/// Validation / reference-resolution errors for a realm connection set.
1532///
1533/// The plan originally listed a `DuplicateId(String)` variant; it's been
1534/// omitted because `RealmConfigSection` uses `BTreeMap<String, ...>` for
1535/// backends/auth/bindings, so duplicate ids within one category are
1536/// impossible at ingestion time. Cross-category id sharing is harmless
1537/// (lookups are category-keyed). If a future code path constructs a
1538/// `RealmConfigSection` programmatically and needs duplicate detection,
1539/// add the variant back alongside the check.
1540#[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// ---------------------------------------------------------------------
1566// Ingestion shapes (what TOML / config files deserialize into)
1567// ---------------------------------------------------------------------
1568
1569/// Ingestion wrapper for `[realm.<id>.*]` TOML tables.
1570///
1571/// The singular nouns `backend`/`auth`/`binding` match TOML dotted-key
1572/// notation (`[realm.dev.backend.openai_default]`) so that one `.backend.X`
1573/// table becomes one entry in the `backend` map.
1574#[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    /// Optional parent realm for config inheritance. Resolved into an ordered
1586    /// chain by [`RealmChain::resolve`]. Schema-invisible: `Config.realm` is
1587    /// wire-projected as an opaque `BTreeMap<String, Value>`, so this typed
1588    /// field never reaches the emitted schemas. A realm with no `parent` that
1589    /// is not itself `global` implicitly inherits from the reserved `global`
1590    /// realm.
1591    #[serde(default, skip_serializing_if = "Option::is_none")]
1592    pub parent: Option<RealmId>,
1593}
1594
1595impl RealmConfigSection {
1596    /// Programmatic constructor for a realm populated from per-provider
1597    /// inline api keys. Used by surfaces (notably the WASM browser
1598    /// runtime) that receive credentials as plain strings at bootstrap
1599    /// and need to translate them into the realm-based config shape
1600    /// consumed by `AgentFactory::build_agent`.
1601    ///
1602    /// For each (provider, secret) pair, emits:
1603    ///   - a `BackendProfileConfig` whose `backend_kind` is the provider's
1604    ///     default kind from the typed provider-matrix enum
1605    ///     (`AnthropicBackendKind::AnthropicApi`, etc.)
1606    ///   - an `AuthProfileConfig` with `CredentialSourceSpec::InlineSecret`
1607    ///   - a `ProviderBindingConfig` wiring the two
1608    ///
1609    /// The first provider in the input list becomes the
1610    /// `default_binding` so that build_agent's auth_binding-less
1611    /// code path can resolve through this realm. Plan §6.10 replacement
1612    /// for the deleted `ProviderSettings.api_keys` map.
1613    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            // Derive the (backend_kind, auth_method) inline-key default pair from
1622            // the typed provider-matrix enums that own each canonical string.
1623            // `other =>` stays the open-world fallback: an unrecognized provider
1624            // name carries its own slug as backend_kind with the conventional
1625            // `api_key` auth method, since no typed matrix enum owns it.
1626            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                    // Every minted binding is the per-provider default; the
1670                    // "default for provider X" fact is carried by this typed
1671                    // marker, not the `default_<provider>` id convention.
1672                    provider_default: true,
1673                },
1674            );
1675            if idx == 0 {
1676                // The first provider also seeds the single per-realm default.
1677                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` has no typed backend/auth-method matrix enum (it is
1722        // the open-world fallback provider), so these literals have no enum to
1723        // derive from. They stay as the sole untyped owner of the
1724        // `other_api` / `api_key` default pair.
1725        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/// Serialized backend profile (pre-normalization).
1813#[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/// Serialized auth profile (pre-normalization).
1825#[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/// Serialized binding (pre-normalization).
1838#[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    /// Marks this binding as the default for its provider. See
1848    /// [`ProviderBinding::provider_default`].
1849    #[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    // ---- Realm inheritance RCTs (parent chain + reserved global) ----------
1860
1861    fn rid(s: &str) -> RealmId {
1862        RealmId::parse(s).expect("valid realm slug")
1863    }
1864
1865    /// Build a Config whose `realm` map holds the given `(id, parent)` pairs.
1866    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    // RCT-01
1885    #[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    // RCT-02
1900    #[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    // RCT-03
1917    #[test]
1918    fn realm_chain_resolves_linear_order_with_implicit_global_tail() {
1919        // child -> team (no parent) ; global configured -> implicit tail.
1920        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        // explicit parent==global terminates without double-visiting global.
1925        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        // head==global terminates as a single node (no self-tail).
1930        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    // RCT-04
1936    #[test]
1937    fn realm_chain_detects_cycle_depth_missing_global_and_env_default() {
1938        // self-parent -> Cycle
1939        let cfg = config_with(&[("a", Some("a"))]);
1940        assert!(matches!(
1941            RealmChain::resolve(&cfg, &rid("a")),
1942            Err(RealmChainError::Cycle { .. })
1943        ));
1944
1945        // A -> B -> A -> Cycle
1946        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        // parent not configured (and not global) -> MissingParent
1953        let cfg = config_with(&[("a", Some("ghost"))]);
1954        assert!(matches!(
1955            RealmChain::resolve(&cfg, &rid("a")),
1956            Err(RealmChainError::MissingParent { .. })
1957        ));
1958
1959        // global with a parent -> GlobalHasParent
1960        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        // a child reaching a global-that-has-a-parent also fails closed
1967        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        // parent == env_default slug -> ParentIsEnvDefault
1978        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        // chain longer than MAX_REALM_CHAIN_DEPTH -> DepthExceeded
1985        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    // RCT-05
2012    #[test]
2013    fn realm_chain_omits_absent_global_and_terminates_at_explicit_root() {
2014        // No [realm.global] configured -> no implicit tail appended.
2015        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    // RCT-26
2025    #[test]
2026    fn absent_head_realm_yields_single_node_chain_then_implicit_tail() {
2027        // Head absent from config, global present -> [head, global].
2028        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        // Head absent, no global -> [head] alone (contributes nothing; resolver
2033        // falls through to env_default downstream).
2034        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        // Wrong component count.
2053        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        // Empty component.
2062        assert!(matches!(
2063            MemberCommsName::from_str("team//alice"),
2064            Err(MemberCommsNameError::InvalidComponent { .. })
2065        ));
2066        // Leading digit / disallowed first char (folds is_valid_peer_name_component).
2067        assert!(MemberCommsName::from_str("1team/reviewer/alice").is_err());
2068        // Disallowed char.
2069        assert!(MemberCommsName::from_str("te.am/reviewer/alice").is_err());
2070        // Underscore-first is allowed.
2071        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        // The component rule is strictly tighter than validate_slug, so any
2077        // valid comms name yields a parseable realm via the shared helper.
2078        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        // Configured origin is the default and is skipped on the wire so the
2175        // shape stays additive for old readers.
2176        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        // Synthetic env-default origin is carried by the typed discriminant,
2184        // not recovered from the realm/binding slug text.
2185        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        // Same slugs, configured origin → NOT an env-default. Proves the
2194        // decision keys on the typed origin, not on "env_default"/"default".
2195        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        // Synthetic origin survives a serde round-trip.
2204        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        // A row without an `origin` field reads back as Configured.
2210        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", &section).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", &section).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    // An EXPLICITLY named binding whose provider disagrees with the requested
2456    // provider is a strict ProviderMismatch (explicit requests are not
2457    // provider-filtered). Default-selection by contrast simply yields no
2458    // candidate for a provider the realm has no binding for (see RCT-10).
2459    #[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    // ---- P2: chain-aware resolution + owning-realm provenance -------------
2487
2488    /// global owns the openai binding `primary`; `child` inherits via parent.
2489    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    // RCT-06
2516    #[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    // RCT-07
2537    #[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        // The registry equality (auth_binding.realm == realm.realm_id) holds for
2551        // an inherited binding WITHOUT any relaxation.
2552        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    // RCT-08
2560    #[test]
2561    fn inherited_binding_resolves_backend_auth_in_owning_realm_only() {
2562        // child redefines the SAME auth-profile key as a DIFFERENT provider.
2563        // The inherited binding (owned by global) must resolve global's auth,
2564        // never child's shadow (binding-owner == auth-owner invariant).
2565        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    // RCT-10
2591    #[test]
2592    fn single_target_path_uses_unified_selection_policy() {
2593        // A realm with NO default_binding but a single unambiguous provider
2594        // binding resolves it (old default_binding-only path would have failed).
2595        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    // RCT-11
2629    #[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    // RCT-25
2651    #[test]
2652    fn default_realm_literal_not_consulted_then_works_under_global() {
2653        // [realm.default] is NOT auto-consulted for an unrelated head.
2654        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        // The SAME binding under [realm.global] IS inherited via the implicit tail.
2692        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    // RCT-35
2726    #[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    // RCT-38
2784    #[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    // Strict-owner write (decision 5) — single core policy consumed by REST/RPC.
2805    #[test]
2806    fn resolve_write_owner_classifies_owned_inherited_and_unknown() {
2807        let cfg = openai_inherit_config(""); // global owns "primary"; child parent=global
2808        let primary = BindingId::parse("primary").unwrap();
2809
2810        // Head owns the binding in its OWN section -> write allowed at head.
2811        assert_eq!(
2812            resolve_write_owner(&cfg, &rid("global"), &primary)
2813                .expect("global owns primary")
2814                .as_str(),
2815            "global"
2816        );
2817
2818        // Inherited by child -> rejected, naming the owning realm.
2819        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        // Not defined anywhere on the chain -> Unknown.
2826        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    // RCT-09: the flat cross-realm scan is gone. An unrelated sibling realm
2870    // (not an ancestor of the head) is NOT auto-discovered; only chain members
2871    // + env_default are candidates.
2872    #[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        // Head 'missing' is absent, has no parent edge, and there is no global
2893        // realm — so 'dev' is an unrelated sibling and must NOT be discovered.
2894        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        // Only the synthetic env-var fallback remains.
2912        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        // Every provider's minted binding carries the typed per-provider
2927        // default marker — not just the first (idx==0) one.
2928        assert!(section.binding["default_anthropic"].provider_default);
2929        assert!(section.binding["default_openai"].provider_default);
2930        // The first provider still seeds the single per-realm default.
2931        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        // Two openai bindings; the second is marked provider_default. The
2940        // selector must pick by the typed marker, not by any id name.
2941        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(&section).unwrap();
2982        // All maps empty + no default_binding → empty object.
2983        assert_eq!(s, "{}");
2984    }
2985
2986    #[test]
2987    fn realm_config_section_serde_populated() {
2988        // `default_binding` appears BEFORE any section header so that TOML
2989        // treats it as a top-level field rather than a key inside the last
2990        // subsection.
2991        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}