cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

provider.rs (16717B)


      1 #[cfg(test)]
      2 use crate::runtime::config::{
      3     CapabilityBindingInspection, CapabilityBindingInspectionState, INFERENCE_HYF_STDIO_CAPABILITY,
      4 };
      5 use crate::runtime::config::{PublishTransport, RuntimeConfig};
      6 #[cfg(test)]
      7 use crate::runtime::hyf;
      8 use crate::view::runtime::PublishRuntimeView;
      9 
     10 #[cfg(test)]
     11 const WRITE_PLANE_TARGET_DETAIL: &str =
     12     "write-plane targets are resolved by mode-specific publish commands";
     13 
     14 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     15 pub enum ProviderProvenance {
     16     #[cfg(test)]
     17     ExplicitBinding,
     18     #[cfg(test)]
     19     ManagedDefault,
     20     #[cfg(test)]
     21     DirectConfig,
     22     #[cfg(test)]
     23     Disabled,
     24     PublishTransport,
     25     #[cfg(test)]
     26     Unavailable,
     27 }
     28 
     29 impl ProviderProvenance {
     30     pub fn as_str(self) -> &'static str {
     31         match self {
     32             #[cfg(test)]
     33             Self::ExplicitBinding => "explicit_binding",
     34             #[cfg(test)]
     35             Self::ManagedDefault => "managed_default",
     36             #[cfg(test)]
     37             Self::DirectConfig => "direct_config",
     38             #[cfg(test)]
     39             Self::Disabled => "disabled",
     40             Self::PublishTransport => "publish_transport",
     41             #[cfg(test)]
     42             Self::Unavailable => "unavailable",
     43         }
     44     }
     45 }
     46 
     47 #[cfg(test)]
     48 #[derive(Debug, Clone, PartialEq, Eq)]
     49 pub struct ResolvedProviderView {
     50     pub capability_id: String,
     51     pub provider_runtime_id: String,
     52     pub binding_model: String,
     53     pub state: String,
     54     pub provenance: String,
     55     pub source: String,
     56     pub target_kind: Option<String>,
     57     pub target: Option<String>,
     58 }
     59 
     60 #[derive(Debug, Clone, PartialEq, Eq)]
     61 pub struct WritePlaneProviderView {
     62     pub provider_runtime_id: String,
     63     pub binding_model: String,
     64     pub state: String,
     65     pub provenance: String,
     66     pub source: String,
     67     pub target_kind: Option<String>,
     68     pub target: Option<String>,
     69     pub detail: String,
     70 }
     71 
     72 #[cfg(test)]
     73 #[derive(Debug, Clone, PartialEq, Eq)]
     74 pub struct ResolvedWritePlaneTarget {
     75     pub url: String,
     76 }
     77 
     78 #[cfg(test)]
     79 #[derive(Debug, Clone, PartialEq, Eq)]
     80 pub struct HyfProviderView {
     81     pub provider_runtime_id: String,
     82     pub binding_model: String,
     83     pub state: String,
     84     pub provenance: String,
     85     pub source: String,
     86     pub target_kind: Option<String>,
     87     pub target: Option<String>,
     88     pub executable: String,
     89     pub reason: Option<String>,
     90     pub protocol_version: Option<u64>,
     91     pub deterministic_available: Option<bool>,
     92 }
     93 
     94 pub fn resolve_write_plane_provider(
     95     config: &RuntimeConfig,
     96     publish: &PublishRuntimeView,
     97 ) -> WritePlaneProviderView {
     98     let (provider_runtime_id, binding_model, detail) = match config.publish.transport {
     99         PublishTransport::DirectNostrRelay => (
    100             "direct_nostr_relay",
    101             "direct_relay_publish",
    102             "direct relay publish is selected; readiness is reported under publish",
    103         ),
    104         PublishTransport::RadrootsdProxy => (
    105             "radrootsd_proxy",
    106             "daemon_proxy_publish",
    107             "radrootsd_proxy publish is selected; readiness is reported under publish",
    108         ),
    109     };
    110     WritePlaneProviderView {
    111         provider_runtime_id: provider_runtime_id.to_owned(),
    112         binding_model: binding_model.to_owned(),
    113         state: publish.state.clone(),
    114         provenance: ProviderProvenance::PublishTransport.as_str().to_owned(),
    115         source: publish.source.clone(),
    116         target_kind: None,
    117         target: None,
    118         detail: publish.reason.clone().unwrap_or_else(|| detail.to_owned()),
    119     }
    120 }
    121 
    122 #[cfg(test)]
    123 pub fn resolve_actor_write_plane_target(
    124     config: &RuntimeConfig,
    125 ) -> Result<ResolvedWritePlaneTarget, String> {
    126     let _ = config;
    127     Err(WRITE_PLANE_TARGET_DETAIL.to_owned())
    128 }
    129 
    130 #[cfg(test)]
    131 pub fn resolve_hyf_provider(config: &RuntimeConfig) -> HyfProviderView {
    132     let binding = inspect_binding(config, INFERENCE_HYF_STDIO_CAPABILITY);
    133     let status = hyf::resolve_runtime_status(config);
    134     let binding_configured = binding.state == CapabilityBindingInspectionState::Configured;
    135     let provenance = if binding_configured {
    136         binding_provenance(&binding)
    137     } else if status.state == "disabled" {
    138         ProviderProvenance::Disabled
    139     } else {
    140         ProviderProvenance::DirectConfig
    141     }
    142     .as_str()
    143     .to_owned();
    144     let target_kind = hyf_target_kind(config, &binding);
    145     let target = hyf_target(config, &binding);
    146     let executable = hyf_executable(config, &binding, &status);
    147     let source = if binding_configured {
    148         binding.source.clone()
    149     } else {
    150         status.source.clone()
    151     };
    152 
    153     HyfProviderView {
    154         provider_runtime_id: binding.provider_runtime_id,
    155         binding_model: binding.binding_model,
    156         state: status.state,
    157         provenance,
    158         source,
    159         target_kind,
    160         target,
    161         executable,
    162         reason: status.reason,
    163         protocol_version: status.protocol_version,
    164         deterministic_available: status.deterministic_available,
    165     }
    166 }
    167 
    168 #[cfg(test)]
    169 pub fn resolve_capability_providers(config: &RuntimeConfig) -> Vec<ResolvedProviderView> {
    170     let hyf = resolve_hyf_provider(config);
    171 
    172     vec![ResolvedProviderView {
    173         capability_id: INFERENCE_HYF_STDIO_CAPABILITY.to_owned(),
    174         provider_runtime_id: hyf.provider_runtime_id,
    175         binding_model: hyf.binding_model,
    176         state: hyf.state,
    177         provenance: hyf.provenance,
    178         source: hyf.source,
    179         target_kind: hyf.target_kind,
    180         target: hyf.target,
    181     }]
    182 }
    183 
    184 #[cfg(test)]
    185 fn inspect_binding(config: &RuntimeConfig, capability_id: &str) -> CapabilityBindingInspection {
    186     config
    187         .inspect_capability_bindings()
    188         .into_iter()
    189         .find(|binding| binding.capability_id == capability_id)
    190         .expect("provider capability binding inspection must exist")
    191 }
    192 
    193 #[cfg(test)]
    194 fn binding_provenance(binding: &CapabilityBindingInspection) -> ProviderProvenance {
    195     match binding.state {
    196         CapabilityBindingInspectionState::Configured => match binding.target_kind.as_deref() {
    197             Some("managed_instance") => ProviderProvenance::ManagedDefault,
    198             _ => ProviderProvenance::ExplicitBinding,
    199         },
    200         CapabilityBindingInspectionState::Disabled => ProviderProvenance::Disabled,
    201         CapabilityBindingInspectionState::NotConfigured => ProviderProvenance::Unavailable,
    202     }
    203 }
    204 
    205 #[cfg(test)]
    206 fn hyf_target_kind(
    207     config: &RuntimeConfig,
    208     binding: &CapabilityBindingInspection,
    209 ) -> Option<String> {
    210     if binding.state == CapabilityBindingInspectionState::Configured {
    211         return binding.target_kind.clone();
    212     }
    213     if config.hyf.enabled {
    214         return Some("direct_config".to_owned());
    215     }
    216     None
    217 }
    218 
    219 #[cfg(test)]
    220 fn hyf_target(config: &RuntimeConfig, binding: &CapabilityBindingInspection) -> Option<String> {
    221     if binding.state == CapabilityBindingInspectionState::Configured {
    222         return binding.target.clone();
    223     }
    224     if config.hyf.enabled {
    225         return Some(config.hyf.executable.display().to_string());
    226     }
    227     None
    228 }
    229 
    230 #[cfg(test)]
    231 fn hyf_executable(
    232     config: &RuntimeConfig,
    233     binding: &CapabilityBindingInspection,
    234     status: &hyf::HyfStatusView,
    235 ) -> String {
    236     if binding.state == CapabilityBindingInspectionState::Configured
    237         && binding.target_kind.as_deref() == Some("explicit_endpoint")
    238     {
    239         return binding
    240             .target
    241             .clone()
    242             .unwrap_or_else(|| status.executable.clone());
    243     }
    244     if !config.hyf.enabled {
    245         return status.executable.clone();
    246     }
    247     status.executable.clone()
    248 }
    249 
    250 #[cfg(test)]
    251 mod tests {
    252     use std::path::PathBuf;
    253 
    254     use radroots_runtime_paths::RadrootsMigrationReport;
    255     use radroots_secret_vault::RadrootsSecretBackend;
    256 
    257     use super::{
    258         ProviderProvenance, resolve_actor_write_plane_target, resolve_capability_providers,
    259         resolve_hyf_provider, resolve_write_plane_provider,
    260     };
    261     use crate::runtime::config::{
    262         AccountConfig, AccountSecretContractConfig, CapabilityBindingConfig,
    263         CapabilityBindingSource, CapabilityBindingTargetKind, HyfConfig, IdentityConfig,
    264         InteractionConfig, LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig,
    265         OutputFormat, PathsConfig, PublishConfig, PublishTransport, PublishTransportSource,
    266         RelayConfig, RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig,
    267         SignerBackend, SignerConfig, Verbosity,
    268     };
    269     use crate::view::runtime::{
    270         PublishProviderRuntimeView, PublishRelayRuntimeView, PublishRuntimeView,
    271     };
    272 
    273     fn sample_config(bindings: Vec<CapabilityBindingConfig>, hyf_enabled: bool) -> RuntimeConfig {
    274         RuntimeConfig {
    275             output: OutputConfig {
    276                 format: OutputFormat::Human,
    277                 verbosity: Verbosity::Normal,
    278                 color: true,
    279                 dry_run: false,
    280             },
    281             interaction: InteractionConfig {
    282                 input_enabled: true,
    283                 assume_yes: false,
    284                 stdin_tty: true,
    285                 stdout_tty: true,
    286                 prompts_allowed: true,
    287                 confirmations_allowed: true,
    288             },
    289             paths: PathsConfig {
    290                 profile: "interactive_user".into(),
    291                 profile_source: "default".into(),
    292                 allowed_profiles: vec!["interactive_user".into()],
    293                 root_source: "host_defaults".into(),
    294                 repo_local_root: None,
    295                 repo_local_root_source: None,
    296                 subordinate_path_override_source: "runtime_config".into(),
    297                 app_namespace: "apps/cli".into(),
    298                 shared_accounts_namespace: "shared/accounts".into(),
    299                 shared_identities_namespace: "shared/identities".into(),
    300                 app_config_path: PathBuf::from("/tmp/config/apps/cli/config.toml"),
    301                 workspace_config_path: None,
    302                 app_data_root: PathBuf::from("/tmp/data"),
    303                 app_logs_root: PathBuf::from("/tmp/logs"),
    304                 shared_accounts_data_root: PathBuf::from("/tmp/shared/accounts"),
    305                 shared_accounts_secrets_root: PathBuf::from("/tmp/shared/accounts-secrets"),
    306                 default_identity_path: PathBuf::from("/tmp/default-identity.json"),
    307             },
    308             migration: MigrationConfig {
    309                 report: RadrootsMigrationReport::empty(),
    310             },
    311             logging: LoggingConfig {
    312                 filter: "info".into(),
    313                 directory: None,
    314                 stdout: true,
    315             },
    316             account: AccountConfig {
    317                 selector: None,
    318                 store_path: PathBuf::from("/tmp/store.json"),
    319                 secrets_dir: PathBuf::from("/tmp/secrets"),
    320                 secret_backend: RadrootsSecretBackend::EncryptedFile,
    321                 secret_fallback: None,
    322             },
    323             account_secret_contract: AccountSecretContractConfig {
    324                 default_backend: "host_vault".into(),
    325                 default_fallback: Some("encrypted_file".into()),
    326                 allowed_backends: vec!["host_vault".into(), "encrypted_file".into()],
    327                 host_vault_policy: Some("desktop".into()),
    328                 uses_protected_store: true,
    329             },
    330             identity: IdentityConfig {
    331                 path: PathBuf::from("/tmp/default-identity.json"),
    332             },
    333             signer: SignerConfig {
    334                 backend: SignerBackend::Local,
    335             },
    336             publish: PublishConfig {
    337                 transport: PublishTransport::DirectNostrRelay,
    338                 source: PublishTransportSource::Defaults,
    339                 radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
    340             },
    341             relay: RelayConfig {
    342                 urls: Vec::new(),
    343                 publish_policy: RelayPublishPolicy::Any,
    344                 source: RelayConfigSource::Defaults,
    345             },
    346             local: LocalConfig {
    347                 root: PathBuf::from("/tmp/local"),
    348                 replica_db_path: PathBuf::from("/tmp/local/replica.sqlite"),
    349                 backups_dir: PathBuf::from("/tmp/local/backups"),
    350                 exports_dir: PathBuf::from("/tmp/local/exports"),
    351             },
    352             myc: MycConfig {
    353                 executable: PathBuf::from("myc"),
    354                 status_timeout_ms: 2_000,
    355             },
    356             hyf: HyfConfig {
    357                 enabled: hyf_enabled,
    358                 executable: PathBuf::from("hyfd"),
    359             },
    360             rpc: RpcConfig {
    361                 url: "http://127.0.0.1:7070".into(),
    362             },
    363             rhi: crate::runtime::config::RhiConfig {
    364                 trusted_worker_pubkeys: Vec::new(),
    365             },
    366             capability_bindings: bindings,
    367         }
    368     }
    369 
    370     fn publish_view(
    371         config: &RuntimeConfig,
    372         state: &str,
    373         reason: Option<&str>,
    374     ) -> PublishRuntimeView {
    375         PublishRuntimeView {
    376             transport: config.publish.transport.as_str().to_owned(),
    377             source: config.publish.source.as_str().to_owned(),
    378             transport_family: config.publish.transport.transport_family().to_owned(),
    379             state: state.to_owned(),
    380             executable: state == "ready",
    381             reason: reason.map(str::to_owned),
    382             signed_write_required: true,
    383             relay: PublishRelayRuntimeView {
    384                 ready: !config.relay.urls.is_empty(),
    385                 count: config.relay.urls.len(),
    386                 source: config.relay.source.as_str().to_owned(),
    387             },
    388             provider: PublishProviderRuntimeView {
    389                 provider_runtime_id: config.publish.transport.as_str().to_owned(),
    390                 state: state.to_owned(),
    391                 source: config.publish.source.as_str().to_owned(),
    392                 reason: reason.map(str::to_owned),
    393             },
    394         }
    395     }
    396 
    397     #[test]
    398     fn write_plane_provider_tracks_direct_relay_publish() {
    399         let config = sample_config(Vec::new(), false);
    400         let publish = publish_view(
    401             &config,
    402             "unconfigured",
    403             Some("direct_nostr_relay publish transport requires a configured relay"),
    404         );
    405         let view = resolve_write_plane_provider(&config, &publish);
    406         assert_eq!(view.provider_runtime_id, "direct_nostr_relay");
    407         assert_eq!(view.binding_model, "direct_relay_publish");
    408         assert_eq!(view.state, "unconfigured");
    409         assert_eq!(
    410             view.provenance,
    411             ProviderProvenance::PublishTransport.as_str()
    412         );
    413         assert!(view.target.is_none());
    414         assert!(view.detail.contains("configured relay"));
    415     }
    416 
    417     #[test]
    418     fn actor_write_plane_target_fails_closed() {
    419         let error = resolve_actor_write_plane_target(&sample_config(Vec::new(), false))
    420             .expect_err("write plane target");
    421         assert_eq!(
    422             error,
    423             "write-plane targets are resolved by mode-specific publish commands"
    424         );
    425     }
    426 
    427     #[test]
    428     fn hyf_uses_direct_config_when_enabled_without_binding() {
    429         let view = resolve_hyf_provider(&sample_config(Vec::new(), true));
    430         assert_eq!(view.provenance, ProviderProvenance::DirectConfig.as_str());
    431         assert_eq!(view.target_kind.as_deref(), Some("direct_config"));
    432         assert_eq!(view.target.as_deref(), Some("hyfd"));
    433     }
    434 
    435     #[test]
    436     fn hyf_binding_remains_visible_when_runtime_is_disabled() {
    437         let binding = CapabilityBindingConfig {
    438             capability_id: "inference.hyf_stdio".into(),
    439             provider_runtime_id: "hyf".into(),
    440             binding_model: "stdio_service".into(),
    441             source: CapabilityBindingSource::UserConfig,
    442             target_kind: CapabilityBindingTargetKind::ExplicitEndpoint,
    443             target: "bin/hyfd-user".into(),
    444             managed_account_ref: None,
    445             signer_session_ref: None,
    446         };
    447         let view = resolve_hyf_provider(&sample_config(vec![binding], false));
    448         assert_eq!(view.state, "disabled");
    449         assert_eq!(
    450             view.provenance,
    451             ProviderProvenance::ExplicitBinding.as_str()
    452         );
    453         assert_eq!(view.source, "user config [[capability_binding]]");
    454         assert_eq!(view.target_kind.as_deref(), Some("explicit_endpoint"));
    455         assert_eq!(view.target.as_deref(), Some("bin/hyfd-user"));
    456         assert_eq!(view.executable, "bin/hyfd-user");
    457     }
    458 
    459     #[test]
    460     fn capability_provider_list_only_covers_active_hyf_provider() {
    461         let providers = resolve_capability_providers(&sample_config(Vec::new(), false));
    462         assert_eq!(providers.len(), 1);
    463         assert_eq!(providers[0].capability_id, "inference.hyf_stdio");
    464     }
    465 }