difftreelog
ci use in-tree remowt-agent
42 files changed
Cargo.lockdiffbeforeafterboth722722723[[package]]723[[package]]724name = "camino"724name = "camino"725version = "1.2.2"725version = "1.2.3"726source = "registry+https://github.com/rust-lang/crates.io-index"726source = "registry+https://github.com/rust-lang/crates.io-index"727checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"727checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f"728dependencies = [728dependencies = [729 "serde_core",729 "serde_core",730]730]1899 "fleet-shared",1899 "fleet-shared",1900 "hex",1900 "hex",1901 "hmac 0.13.0",1901 "hmac 0.13.0",1902 "pbkdf2 0.12.2",1902 "pbkdf2 0.13.0",1903 "rand 0.10.1",1903 "rand 0.10.1",1904 "sha2 0.10.9",1904 "sha2 0.11.0",1905 "x25519-dalek",1905 "x25519-dalek",1906]1906]190719075011 "nix-eval",5011 "nix-eval",5012 "remowt-client",5012 "remowt-client",5013 "remowt-endpoints",5013 "remowt-endpoints",5014 "remowt-link-shared",5014 "serde",5015 "serde",5015 "thiserror 2.0.18",5016 "thiserror 2.0.18",5016 "tokio",5017 "tokio",5031 "iroh-base",5032 "iroh-base",5032 "n0-watcher",5033 "n0-watcher",5033 "noq-udp",5034 "noq-udp",5034 "remowt-ui-prompt",5035 "serde",5035 "serde",5036 "serde_json",5036 "serde_json",5037 "thiserror 2.0.18",5037 "thiserror 2.0.18",5038 "tokio",5038 "tokio",5039 "tokio-util",5039 "tokio-util",5040 "tracing",5040 "tracing",5041 "uuid",5041]5042]504250435043[[package]]5044[[package]]5093 "anyhow",5094 "anyhow",5094 "bifrostlink",5095 "bifrostlink",5095 "bifrostlink-macros",5096 "bifrostlink-macros",5097 "remowt-link-shared",5096 "serde",5098 "serde",5097 "thiserror 2.0.18",5099 "thiserror 2.0.18",5098 "tokio",5100 "tokio",5099 "tracing",5101 "tracing",5100 "zbus",5101]5102]510251035103[[package]]5104[[package]]Cargo.tomldiffbeforeafterboth15remowt-fleet = { path = "./crates/remowt-fleet" }15remowt-fleet = { path = "./crates/remowt-fleet" }161617remowt-client = { version = "0.1.9", path = "remowt/crates/remowt-client" }17remowt-client = { version = "0.1.9", path = "remowt/crates/remowt-client" }18remowt-polkit-shared = { version = "0.1.9", path = "remowt/crates/polkit-shared" }18remowt-endpoints = { version = "0.1.9", path = "remowt/crates/remowt-endpoints" }19remowt-link-shared = { version = "0.1.9", path = "remowt/crates/remowt-link-shared" }19remowt-link-shared = { version = "0.1.9", path = "remowt/crates/remowt-link-shared" }20remowt-plugin = { version = "0.1.9", path = "remowt/crates/remowt-plugin" }20remowt-plugin = { version = "0.1.9", path = "remowt/crates/remowt-plugin" }21remowt-polkit-shared = { version = "0.1.9", path = "remowt/crates/polkit-shared" }21remowt-ui-prompt = { version = "0.1.9", path = "remowt/crates/remowt-ui-prompt" }22remowt-ui-prompt = { version = "0.1.9", path = "remowt/crates/remowt-ui-prompt" }22remowt-endpoints = { version = "0.1.9", path = "remowt/crates/remowt-endpoints" }232324bifrostlink = "0.2.0"24bifrostlink = "0.2.0"25bifrostlink-macros = "0.2.0"25bifrostlink-macros = "0.2.0"26bifrostlink-ports = "0.2.0"26bifrostlink-ports = "0.2.0"272728iroh = { version = "1.0.0", features = ["unstable-custom-transports"] }28iroh = { version = "1.0.0", features = ["unstable-custom-transports"] }29iroh-base = "1.0.0"29iroh-base = "1.0.0"30noq-udp = { version = "1.0.0", default-features = false }31n0-watcher = "1.0.0"30n0-watcher = "1.0.0"3233uuid = { version = "1", features = ["v4"] }34russh = { version = "0.61.2", default-features = false, features = [31noq-udp = { version = "1.0.0", default-features = false }35 "ring",3236 "flate2",37 "rsa",38] }39russh-config = "0.58.0"40age = { version = "0.11", features = ["plugin", "ssh"] }33age = { version = "0.11", features = ["plugin", "ssh"] }41anyhow = "1.0"34anyhow = "1.0"42base64 = "0.22.1"35base64 = "0.22.1"64opentelemetry-appender-tracing = "0.32.0"57opentelemetry-appender-tracing = "0.32.0"65opentelemetry-otlp = { version = "0.32.0", features = ["grpc-tonic", "gzip-tonic", "http-json", "reqwest-rustls"] }58opentelemetry-otlp = { version = "0.32.0", features = ["grpc-tonic", "gzip-tonic", "http-json", "reqwest-rustls"] }66opentelemetry_sdk = "0.32.0"59opentelemetry_sdk = "0.32.0"67pbkdf2 = "0.12"60pbkdf2 = "0.13"68peg = "0.8.5"61peg = "0.8.5"69pkg-config = "0.3.30"62pkg-config = "0.3.30"70rand = "0.10.0"63rand = "0.10.0"64russh = { version = "0.61.2", default-features = false, features = ["flate2", "ring", "rsa"] }65russh-config = "0.58.0"71serde = { version = "1.0", features = ["derive"] }66serde = { version = "1.0", features = ["derive"] }72serde_json = "1.0"67serde_json = "1.0"73sha2 = "0.10"68sha2 = "0.11"74shlex = "2.0.1"69shlex = "2.0.1"75tabled = "0.21.0"70tabled = "0.21.0"76tempfile = "3.20"71tempfile = "3.20"81tracing = "0.1"76tracing = "0.1"82tracing-indicatif = "0.3.13"77tracing-indicatif = "0.3.13"83tracing-opentelemetry = "0.33.0"78tracing-opentelemetry = "0.33.0"79uuid = { version = "1", features = ["v4"] }84# For fixed coloring needs to be updated to https://github.com/tokio-rs/tracing/pull/348480# For fixed coloring needs to be updated to https://github.com/tokio-rs/tracing/pull/348481tokio-util = "0.7.11"85tracing-subscriber = { version = "0.3.19", features = ["env-filter", "fmt"] }82tracing-subscriber = { version = "0.3.19", features = ["env-filter", "fmt"] }86unicode_categories = "0.1.1"83unicode_categories = "0.1.1"87vte = { version = "0.15.0", features = ["ansi"] }84vte = { version = "0.15.0", features = ["ansi"] }88x25519-dalek = { version = "2.0.1", features = ["getrandom"] }85x25519-dalek = { version = "2.0.1", features = ["getrandom"] }89zbus = "5.16.0"86zbus = "5.16.0"90zbus_polkit = "5.0.0"87zbus_polkit = "5.0.0"91tokio-util = "0.7.11"928893[profile.dev]89[profile.dev]94panic = "abort"90panic = "abort"cmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth5use clap::Parser;5use clap::Parser;6use fleet_base::{6use fleet_base::{7 deploy::{DeployAction, deploy_task, upload_task},7 deploy::{DeployAction, deploy_task, upload_task},8 host::{Config, DeployKind, GenerationStorage},8 host::{Config, DeployKind},9 opts::FleetOpts,9 opts::FleetOpts,10 pins::GenerationStorage,10};11};11use futures::{StreamExt as _, stream::FuturesUnordered};12use futures::{StreamExt as _, stream::FuturesUnordered};12use nix_eval::nix_go;13use nix_eval::nix_go;145 }146 }146147147 let remote_path =148 let remote_path =148 match upload_task(&config, &host, GenerationStorage::Deployer, built).await149 match upload_task(&host, GenerationStorage::Deployer, built).await {149 {150 Ok(v) => v,150 Ok(v) => v,151 Err(e) => {151 Err(e) => {cmds/fleet/src/cmds/rollback.rsdiffbeforeafterboth4use clap::Parser;4use clap::Parser;5use fleet_base::{5use fleet_base::{6 deploy::{DeployAction, deploy_task, upload_task},6 deploy::{DeployAction, deploy_task, upload_task},7 host::{Config, ConfigHost, Generation, GenerationStorage},7 host::{Config, ConfigHost},8 opts::FleetOpts,8 opts::FleetOpts,9 pins::{Generation, GenerationStorage},9};10};10use tabled::Table;11use tabled::Table;11use tracing::{info, warn};12use tracing::{info, warn};104 );105 );105 };106 };106 let remote_path = upload_task(107 let remote_path =107 config,108 &host,108 upload_task(&host, generation.location, generation.store_path.clone()).await?;109 generation.location,110 generation.store_path.clone(),111 )112 .await?;113109cmds/fleet/src/extra_args.rsdiffbeforeafterbothno changes
cmds/fleet/src/main.rsdiffbeforeafterboth1#![recursion_limit = "512"]1#![recursion_limit = "512"]223pub(crate) mod cmds;3pub(crate) mod cmds;4// pub(crate) mod command;5pub(crate) mod extra_args;647use std::{process::ExitCode, sync::Arc};5use std::{process::ExitCode, sync::Arc};86cmds/generator-helper/src/main.rsdiffbeforeafterboth14use clap::{Parser, ValueEnum};14use clap::{Parser, ValueEnum};15use ed25519_dalek::SecretKey;15use ed25519_dalek::SecretKey;16use fleet_shared::SecretData;16use fleet_shared::SecretData;17use hmac::Mac as _;17use hmac::{KeyInit as _, Mac as _};18use rand::{18use rand::{19 Rng as _,19 Rng as _,20 distr::{Alphanumeric, Distribution, SampleString, Uniform},20 distr::{Alphanumeric, Distribution, SampleString, Uniform},338 );338 );339339340 type HmacSha256 = hmac::Hmac<sha2::Sha256>;340 type HmacSha256 = hmac::Hmac<sha2::Sha256>;341 let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(&salted)341 let mut mac =342 .expect("HMAC accepts any key length");342 HmacSha256::new_from_slice(&salted).expect("HMAC accepts any key length");343 mac.update(b"Client Key");343 mac.update(b"Client Key");344 let client_key = mac.finalize().into_bytes();344 let client_key = mac.finalize().into_bytes();347 hasher.update(client_key);347 hasher.update(client_key);348 let stored_key = hasher.finalize();348 let stored_key = hasher.finalize();349349350 let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(&salted)350 let mut mac =351 .expect("HMAC accepts any key length");351 HmacSha256::new_from_slice(&salted).expect("HMAC accepts any key length");352 mac.update(b"Server Key");352 mac.update(b"Server Key");353 let server_key = mac.finalize().into_bytes();353 let server_key = mac.finalize().into_bytes();cmds/remowt-plugin-fleet/Cargo.tomldiffbeforeafterboth2name = "remowt-plugin-fleet"2name = "remowt-plugin-fleet"3description = "Remowt plugin exposing fleet's Nix endpoint to a running agent"3description = "Remowt plugin exposing fleet's Nix endpoint to a running agent"4version.workspace = true4version.workspace = true5edition = "2021"5edition.workspace = true667[dependencies]7[dependencies]8remowt-fleet.workspace = true8remowt-fleet.workspace = truecrates/fleet-base/src/deploy.rsdiffbeforeafterboth11use tokio::time::sleep;11use tokio::time::sleep;12use tracing::{Instrument as _, error, info, info_span, warn};12use tracing::{Instrument as _, error, info, info_span, warn};131314use crate::host::{Config, ConfigHost, DeployKind, Generation, GenerationStorage};14use crate::host::{ConfigHost, DeployKind};15use crate::pins::{Generation, GenerationStorage};151616#[derive(ValueEnum, Clone, Copy)]17#[derive(ValueEnum, Clone, Copy)]17pub enum DeployAction {18pub enum DeployAction {260 {261 {261 // It is ok, if there was no reboot - then timer might not be running.262 // It is ok, if there was no reboot - then timer might not be running.262 }263 }263 if action.should_schedule_rollback_run() {264 if action.should_schedule_rollback_run()264 if let Err(e) = elevated_systemd265 && let Err(e) = elevated_systemd265 .stop("rollback-watchdog-run.timer".to_owned())266 .stop("rollback-watchdog-run.timer".to_owned())266 .await267 .await267 {268 {268 error!("failed to disarm rollback run: {e}");269 error!("failed to disarm rollback run: {e}");269 }270 }270 }271 } else if let Err(_e) = elevated_fs271 } else if let Err(_e) = elevated_fs272 .rm_file(Utf8PathBuf::from("/etc/fleet_rollback_marker"))272 .rm_file(Utf8PathBuf::from("/etc/fleet_rollback_marker"))273 .in_current_span()273 .in_current_span()280}280}281281282pub async fn upload_task(282pub async fn upload_task(283 config: &Config,284 host: &ConfigHost,283 host: &ConfigHost,285 location: GenerationStorage,284 location: GenerationStorage,286 generation: Utf8PathBuf,285 generation: Utf8PathBuf,crates/fleet-base/src/fleetdata.rsdiffbeforeafterboth6 },6 },7 fmt,7 fmt,8 io::{self, Cursor},8 io::{self, Cursor},9 str::FromStr,9 sync::RwLock,10 sync::RwLock,10};11};111292 #[serde(skip_serializing)]93 #[serde(skip_serializing)]93 host_secrets: BTreeMap<SecretOwner, BTreeMap<String, FleetSecretDistribution>>,94 host_secrets: BTreeMap<SecretOwner, BTreeMap<String, FleetSecretDistribution>>,94}95}95impl FleetData {96impl FromStr for FleetData {97 type Err = anyhow::Error;96 pub fn from_str(s: &str) -> anyhow::Result<Self> {98 fn from_str(s: &str) -> anyhow::Result<Self> {97 let mut data: Self = nixlike::parse_str(s)?;99 let mut data: Self = nixlike::parse_str(s)?;98 if !data.host_secrets.is_empty() {100 if !data.host_secrets.is_empty() {99 info!("migrating host secrets into shared secrets structure");101 info!("migrating host secrets into shared secrets structure");294 /// Drop expired distributions296 /// Drop expired distributions295 fn prune_expired(&mut self, now: DateTime<Utc>) {297 fn prune_expired(&mut self, now: DateTime<Utc>) {296 for ele in self.distributions_mut() {298 for ele in self.distributions_mut() {297 if let Some(expires_at) = ele.secret.expires_at {299 if let Some(expires_at) = ele.secret.expires_at298 if expires_at < now {300 && expires_at < now301 {299 ele.prune(format!("expired during check at {now}"));302 ele.prune(format!("expired during check at {now}"));300 }303 }301 }302 }304 }303 }305 }304 /// Perform all pruning relevant to shared secrets306 /// Perform all pruning relevant to shared secrets305 /// Also see expected_owner_removed307 /// Also see expected_owner_removed308 #[allow(clippy::too_many_arguments)]306 pub fn prune_shared(309 pub fn prune_shared(307 &mut self,310 &mut self,308 expected_owners: &BTreeSet<SecretOwner>,311 expected_owners: &BTreeSet<SecretOwner>,323 let mut to_add = expected_owners.difference(¤t_owners);326 let mut to_add = expected_owners.difference(¤t_owners);324 if to_add.next().is_some() && unique && regenerate_on_owner_added {327 if to_add.next().is_some() && unique && regenerate_on_owner_added {325 for dist in self.distributions_mut() {328 for dist in self.distributions_mut() {326 dist.prune(format!(329 dist.prune(327 "owners missing, can't add new distribution, regeneration preferred"330 "owners missing, can't add new distribution, regeneration preferred"331 .to_string(),328 ));332 );329 }333 }330 return;334 return;331 }335 }332336333 for to_remove in current_owners.difference(&expected_owners) {337 for to_remove in current_owners.difference(expected_owners) {334 self.entry(to_remove.clone()).remove(338 self.entry(to_remove.clone()).remove(335 regenerate_on_owner_removed,339 regenerate_on_owner_removed,336 "owner was removed from expected owners list, regenerate_on_owner_removed is set"340 "owner was removed from expected owners list, regenerate_on_owner_removed is set"362 ) -> Option<usize> {366 ) -> Option<usize> {363 self.distributions()367 self.distributions()364 .enumerate()368 .enumerate()365 .max_by(|(_, a), (_, b)| {369 .max_by(|(_, a), (_, b)| compare_dists(a, b, prefer_identities, include_pruned_owners))366 compare_dists(&a, &b, prefer_identities, include_pruned_owners)367 })368 .map(|(p, _)| p)370 .map(|(p, _)| p)369 }371 }370 /// Secret wants to be the same on all hosts, leave only one unpruned version of it372 /// Secret wants to be the same on all hosts, leave only one unpruned version of it410 filter_owner: Option<&SecretOwner>,412 filter_owner: Option<&SecretOwner>,411 ) {413 ) {412 'dist: for ele in self.distributions_mut() {414 'dist: for ele in self.distributions_mut() {413 if let Some(filter_owner) = filter_owner {415 if let Some(filter_owner) = filter_owner414 if !ele.owners.contains(filter_owner) {416 && !ele.owners.contains(filter_owner)417 {415 continue;418 continue;416 }417 // Note: secret still can have multiple owners even if it is host-owned419 // Note: secret still can have multiple owners even if it is host-owned418 // in this case we expect that all owners using the same generator, so we can prune distribution for all of them420 // in this case we expect that all owners using the same generator, so we can prune distribution for all of them419 }421 }420 for (name, part) in expected_parts {422 for (name, part) in expected_parts {421 let Some(stored_part) = ele.secret.parts.get(name) else {423 let Some(stored_part) = ele.secret.parts.get(name) else {422 ele.prune(format!("secret definition added new part: {name}"));424 ele.prune(format!("secret definition added new part: {name}"));442 filter_owner: Option<&SecretOwner>,444 filter_owner: Option<&SecretOwner>,443 ) {445 ) {444 for ele in self.distributions_mut() {446 for ele in self.distributions_mut() {445 if let Some(filter_owner) = filter_owner {447 if let Some(filter_owner) = filter_owner446 if !ele.owners.contains(filter_owner) {448 && !ele.owners.contains(filter_owner)449 {447 continue;450 continue;448 }449 // Note: secret still can have multiple owners even if it is host-owned451 // Note: secret still can have multiple owners even if it is host-owned450 // in this case we expect that all owners using the same generator, so we can prune distribution for all of them452 // in this case we expect that all owners using the same generator, so we can prune distribution for all of them451 }453 }452 if ele.secret.generation_data != *expected_generation_data {454 if ele.secret.generation_data != *expected_generation_data {453 ele.prune(format!(455 ele.prune(format!(454 "expected generation data mismatch: {expected_generation_data:?}"456 "expected generation data mismatch: {expected_generation_data:?}"519 }521 }520}522}521523522struct OccupiedDistEntry<'d> {524pub struct OccupiedDistEntry<'d> {523 distributions: &'d mut FleetSecretDistributions,525 distributions: &'d mut FleetSecretDistributions,524 idx: usize,526 idx: usize,525 owners: BTreeSet<SecretOwner>,527 owners: BTreeSet<SecretOwner>,537 owners: self.owners,539 owners: self.owners,538 }540 }539 }541 }540 fn set(self, secret: FleetSecretData, reason: String) -> Self {542 pub fn set(self, secret: FleetSecretData, reason: String) -> Self {541 self.remove(false, reason).set(secret)543 self.remove(false, reason).set(secret)542 }544 }543}545}544struct VacantDistEntry<'d> {546pub struct VacantDistEntry<'d> {545 distributions: &'d mut FleetSecretDistributions,547 distributions: &'d mut FleetSecretDistributions,546 owners: BTreeSet<SecretOwner>,548 owners: BTreeSet<SecretOwner>,547}549}548impl<'d> VacantDistEntry<'d> {550impl<'d> VacantDistEntry<'d> {549 fn set(self, secret: FleetSecretData) -> OccupiedDistEntry<'d> {551 pub fn set(self, secret: FleetSecretData) -> OccupiedDistEntry<'d> {550 let Self {552 let Self {551 distributions,553 distributions,552 owners,554 owners,568 }570 }569}571}570572571enum DistEntry<'d> {573pub enum DistEntry<'d> {572 Vacant(VacantDistEntry<'d>),574 Vacant(VacantDistEntry<'d>),573 Occupied(OccupiedDistEntry<'d>),575 Occupied(OccupiedDistEntry<'d>),574}576}575impl DistEntry<'_> {577impl DistEntry<'_> {576 fn remove(self, whole_dist: bool, reason: String) -> Self {578 pub fn remove(self, whole_dist: bool, reason: String) -> Self {577 match self {579 match self {578 DistEntry::Vacant(_) => self,580 DistEntry::Vacant(_) => self,579 DistEntry::Occupied(o) => Self::Vacant(o.remove(whole_dist, reason)),581 DistEntry::Occupied(o) => Self::Vacant(o.remove(whole_dist, reason)),580 }582 }581 }583 }582 fn set(self, secret: FleetSecretData, reason: String) -> Self {584 pub fn set(self, secret: FleetSecretData, reason: String) -> Self {583 Self::Occupied(match self {585 Self::Occupied(match self {584 DistEntry::Vacant(e) => e.set(secret),586 DistEntry::Vacant(e) => e.set(secret),585 DistEntry::Occupied(e) => e.set(secret, reason),587 DistEntry::Occupied(e) => e.set(secret, reason),683}685}684686685impl FleetSecrets {687impl FleetSecrets {686 pub fn keys(&self) -> btree_map::Keys<String, FleetSecretDistributions> {688 pub fn keys(&self) -> btree_map::Keys<'_, String, FleetSecretDistributions> {687 self.0.keys()689 self.0.keys()688 }690 }689691713 }715 }714716715 pub fn get_or_create(&mut self, secret: &str) -> &mut FleetSecretDistributions {717 pub fn get_or_create(&mut self, secret: &str) -> &mut FleetSecretDistributions {716 self.0718 self.0.entry(secret.to_owned()).or_default()717 .entry(secret.to_owned())718 .or_insert(FleetSecretDistributions::default())719 }719 }720720721 pub fn contains(&self, secret: &str) -> bool {721 pub fn contains(&self, secret: &str) -> bool {crates/fleet-base/src/host.rsdiffbeforeafterboth13use camino::{Utf8Path, Utf8PathBuf};13use camino::{Utf8Path, Utf8PathBuf};14use chrono::{DateTime, Utc};14use chrono::{DateTime, Utc};15use fleet_shared::SecretData;15use fleet_shared::SecretData;16use nix_eval::{Store, Value, eval_store, nix_go, nix_go_json, util::assert_warn};16use nix_eval::{Store, Value, drv::DrvGraph, eval_store, nix_go, nix_go_json, util::assert_warn};17use remowt_client::{AgentBundle, Remowt};17use remowt_client::{AgentBundle, Remowt};18use remowt_endpoints::fs::FsClient;18use remowt_endpoints::fs::FsClient;19use remowt_fleet::NixClient;19use remowt_link_shared::Address;20use remowt_link_shared::BifConfig;20use remowt_ui_prompt::auto::AutoPrompter;21use remowt_ui_prompt::auto::AutoPrompter;21use remowt_ui_prompt::bifrost::PromptEndpoints;22use remowt_ui_prompt::bifrost::PromptEndpoints;22use remowt_ui_prompt::{PrependSourcePrompter, Source};23use remowt_ui_prompt::{PrependSourcePrompter, Source};23use tabled::Tabled;24use tempfile::NamedTempFile;24use tempfile::NamedTempFile;25use time::UtcDateTime;26use tokio::task::spawn_blocking;25use tokio::{sync::OnceCell, task::spawn_blocking};27use tracing::warn;26use tracing::{info, warn};282729use crate::fleetdata::{28use crate::fleetdata::{30 FleetData, FleetSecretData, FleetSecretDistribution, FleetSecretPart, SecretOwner,29 FleetData, FleetSecretData, FleetSecretDistribution, FleetSecretPart, SecretOwner,101 groups: OnceLock<Vec<String>>,100 groups: OnceLock<Vec<String>>,102101103 // TODO: Both of those values are taken from host opts, there should be a cleaner way to specify it102 // TODO: Both of those values are taken from host opts, there should be a cleaner way to specify it104 deploy_kind: OnceLock<DeployKind>,103 deploy_kind: OnceCell<DeployKind>,105 session_destination: OnceLock<String>,104 session_destination: OnceLock<String>,106 legacy_ssh_store: OnceLock<bool>,105 legacy_ssh_store: OnceLock<bool>,107106114 pub local: bool,113 pub local: bool,115 pub remowt: OnceLock<Remowt>,114 pub remowt: OnceLock<Remowt>,116 nix_store: OnceLock<Arc<Store>>,115 nix_store: OnceLock<Arc<Store>>,117 nix_plugin: tokio::sync::OnceCell<()>,116 nix_plugin: OnceCell<()>,118}117}119118120const NIX_PLUGIN_ID: u16 = 2;119const NIX_PLUGIN_ID: u16 = 2;132 AgentBundle::from_dir(agents_dir()?)131 AgentBundle::from_dir(agents_dir()?)133}132}134133135#[derive(Debug, Clone, Copy)]136pub enum GenerationStorage {134enum RemoteDerivationMode {137 Deployer,135 CopySigned,138 Machine,136 Copy,139 Pusher,137 Attic,138 Cachix,140}139}141impl GenerationStorage {142 fn prefix(&self) -> &'static str {143 match self {144 GenerationStorage::Deployer => "deployer.",145 GenerationStorage::Machine => "",146 GenerationStorage::Pusher => "pusher.",147 }148 }149}150151#[derive(Tabled, Debug)]152pub struct Generation {153 #[tabled(rename = "ID", format("{}", self.rollback_id()))]154 pub id: u32,155 #[tabled(rename = "Current")]156 pub current: bool,157 #[tabled(rename = "Created at")]158 pub datetime: UtcDateTime,159 #[tabled(format = "{:?}")]160 pub store_path: Utf8PathBuf,161 #[tabled(skip)]162 pub location: GenerationStorage,163}164impl Generation {165 pub fn rollback_id(&self) -> String {166 format!("{}{}", self.location.prefix(), self.id)167 }168}169140170impl ConfigHost {141impl ConfigHost {171 pub async fn list_generations(&self, profile: &str) -> Result<Vec<Generation>> {172 let plugin_id = self.ensure_nix_plugin().await?;173 let nix = self174 .remowt()175 .await?176 .plugin_endpoints::<remowt_fleet::NixClient<_>>(plugin_id);177 let raw = nix178 .list_generations(profile.to_owned())179 .await180 .map_err(|e| anyhow!("{e:?}"))?181 .map_err(|e| anyhow!("{e}"))?;182 raw.into_iter()183 .map(|g| {184 let id: u32 =185 g.id.try_into()186 .with_context(|| format!("generation id {} doesn't fit in u32", g.id))?;187 let datetime = UtcDateTime::from_unix_timestamp(g.creation_time_unix)188 .with_context(|| {189 format!("invalid generation timestamp {}", g.creation_time_unix)190 })?;191 Ok(Generation {192 id,193 current: g.current,194 datetime,195 store_path: g.store_path,196 location: GenerationStorage::Machine,197 })198 })199 .collect()200 }201202 pub fn set_session_destination(&self, dest: String) {142 pub fn set_session_destination(&self, dest: String) {203 self.session_destination143 self.session_destination215 .expect("legacy ssh store is already set")155 .expect("legacy ssh store is already set")216 }156 }217 pub async fn deploy_kind(&self) -> Result<DeployKind> {157 pub async fn deploy_kind(&self) -> Result<DeployKind> {218 if let Some(kind) = self.deploy_kind.get() {158 self.deploy_kind.get_or_try_init(|| async {219 return Ok(*kind);220 }221 let remowt = self.remowt().await?;159 let remowt = self.remowt().await?;222 let fs = remowt.endpoints::<FsClient<_>>();160 let fs = remowt.endpoints::<FsClient<_>>();223 let is_fleet_managed = match fs.file_exists(Utf8PathBuf::from("/etc/FLEET_HOST")).await {161 let is_fleet_managed = match fs.file_exists(Utf8PathBuf::from("/etc/FLEET_HOST")).await {229 if !is_fleet_managed {167 if !is_fleet_managed {230 bail!(168 bail!(231 "{}",169 "{}",232 indoc::indoc! {"170 indoc::indoc! {"233 host is not marked as managed by fleet171 host is not marked as managed by fleet234 if you're not trying to lustrate/install system from scratch,172 if you're not trying to lustrate/install system from scratch,235 you should either173 you should either236 1. manually create /etc/FLEET_HOST file on the target host,174 1. manually create /etc/FLEET_HOST file on the target host,237 2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet175 2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet238 3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos176 3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos239 for installation use ?deploy_kind=nixos_install / ?deploy_kind=nixos_lustrate 177 for installation use ?deploy_kind=nixos_install / ?deploy_kind=nixos_lustrate 240 "}178 "}241 );179 );242 }180 }243 // TOCTOU is possible244 let _ = self.deploy_kind.set(DeployKind::Fleet);181 Ok(DeployKind::Fleet)245 Ok(*self.deploy_kind.get().expect("deploy kind is just set"))182 }).await.copied()246 }183 }247 async fn connection(&self) -> Result<Remowt> {184 async fn connection(&self) -> Result<Remowt> {248 if let Some(conn) = self.remowt.get() {185 if let Some(conn) = self.remowt.get() {285 self.connection().await222 self.connection().await286 }223 }287224225 pub async fn nix_client(&self) -> Result<NixClient<BifConfig>> {226 let remowt = self.remowt().await?;227 let plugin_id = self.ensure_nix_plugin().await?;228 Ok(remowt.plugin_endpoints(plugin_id))229 }288 pub fn ensure_nix_plugin(&self) -> Pin<Box<dyn Future<Output = Result<u16>> + Send + '_>> {230 pub fn ensure_nix_plugin(&self) -> Pin<Box<dyn Future<Output = Result<u16>> + Send + '_>> {289 Box::pin(async {231 Box::pin(async {290 self.nix_plugin232 self.nix_plugin305 .run0_load_plugin_path(NIX_PLUGIN_ID, bin.as_str())247 .run0_load_plugin_path(NIX_PLUGIN_ID, bin.as_str())306 .await248 .await307 .context("failed to load the fleet nix plugin")?;249 .context("failed to load the fleet nix plugin")?;308 self.remowt()309 .await?310 .rpc()311 .wait_for_connection_to(Address::Plugin(NIX_PLUGIN_ID))312 .await313 .map_err(|_| anyhow!("failed to wait for plugin"))?;314 anyhow::Ok(())250 anyhow::Ok(())315 })251 })316 .await?;252 .await?;407 }343 }408 let sign: Pin<Box<dyn Future<Output = Result<()>> + Send>> = {344 let sign: Pin<Box<dyn Future<Output = Result<()>> + Send>> = {409 let path = path.clone();345 let path = path.clone();346 let graph = DrvGraph::resolve(&eval_store(), &path)347 .context("failed to resolve graph to be uploaded")?;348 info!("signing {} paths", graph.nodes.len());349410 Box::pin(async move {350 Box::pin(async move {411 let local = self.config.local_host();351 let local = self.config.local_host();564 local: true,504 local: true,565 remowt: OnceLock::new(),505 remowt: OnceLock::new(),566 nix_store: OnceLock::new(),506 nix_store: OnceLock::new(),567 nix_plugin: tokio::sync::OnceCell::new(),507 nix_plugin: OnceCell::new(),568 deploy_kind: OnceLock::new(),508 deploy_kind: OnceCell::new(),569 session_destination: OnceLock::new(),509 session_destination: OnceLock::new(),570 legacy_ssh_store: OnceLock::new(),510 legacy_ssh_store: OnceLock::new(),571 })511 })607 local: self.localhost == name,547 local: self.localhost == name,608 remowt: OnceLock::new(),548 remowt: OnceLock::new(),609 nix_store: OnceLock::new(),549 nix_store: OnceLock::new(),610 nix_plugin: tokio::sync::OnceCell::new(),550 nix_plugin: OnceCell::new(),611 deploy_kind: OnceLock::new(),551 deploy_kind: OnceCell::new(),612 session_destination: OnceLock::new(),552 session_destination: OnceLock::new(),613 legacy_ssh_store: OnceLock::new(),553 legacy_ssh_store: OnceLock::new(),614 })554 })crates/fleet-base/src/lib.rsdiffbeforeafterboth3pub mod host;3pub mod host;4mod keys;4mod keys;5pub mod opts;5pub mod opts;6pub mod pins;6pub mod primops;7pub mod primops;78crates/fleet-base/src/pins.rsdiffbeforeafterbothno changes
crates/nixlike/fuzz/Cargo.tomldiffbeforeafterboth4version = "0.0.0"4version = "0.0.0"5authors = ["Automatically generated"]5authors = ["Automatically generated"]6publish = false6publish = false7edition = "2024"7edition.workspace = true889[package.metadata]9[package.metadata]10cargo-fuzz = true10cargo-fuzz = truecrates/remowt-fleet/Cargo.tomldiffbeforeafterboth2name = "remowt-fleet"2name = "remowt-fleet"3description = "Remowt endpoints for fleet"3description = "Remowt endpoints for fleet"4version.workspace = true4version.workspace = true5edition = "2021"5edition.workspace = true667[dependencies]7[dependencies]8bifrostlink.workspace = true8bifrostlink.workspace = true18tokio = { workspace = true, features = ["io-util", "net", "process", "rt"] }18tokio = { workspace = true, features = ["io-util", "net", "process", "rt"] }19tracing.workspace = true19tracing.workspace = true20uuid.workspace = true20uuid.workspace = true21remowt-link-shared.workspace = true2122crates/remowt-fleet/src/lib.rsdiffbeforeafterboth7use nix_eval::eval_store;7use nix_eval::eval_store;8use remowt_client::Remowt;8use remowt_client::Remowt;9use remowt_endpoints::nix_daemon::NixDaemonClient;9use remowt_endpoints::nix_daemon::NixDaemonClient;10use remowt_link_shared::iroh_tunnel::TunnelAddr;10use serde::{Deserialize, Serialize};11use serde::{Deserialize, Serialize};11use tokio::net::UnixListener;12use tokio::net::UnixListener;12use tokio::task::spawn_blocking;13use tokio::task::spawn_blocking;98 continue;99 continue;99 }100 }100 };101 };101 let sock_str = remote_sock.as_str().to_owned();102 match nix.serve_store(store.clone(), sock_str).await {102 match nix103 .serve_store(store.clone(), TunnelAddr::Unix(remote_sock))104 .await105 {103 Ok(Ok(())) => {}106 Ok(Ok(())) => {}flake.lockdiffbeforeafterboth171 "type": "github"171 "type": "github"172 }172 }173 },173 },174 "remowt-agents": {175 "inputs": {176 "crane": [177 "crane"178 ],179 "flake-parts": [180 "flake-parts"181 ],182 "nixpkgs": [183 "nixpkgs"184 ],185 "rust-overlay": "rust-overlay",186 "shelly": [187 "shelly"188 ]189 },190 "locked": {191 "lastModified": 1781491763,192 "ref": "refs/heads/trunk",193 "rev": "8b18e8b236d428ed8fbf5818272d17f5f538c13c",194 "revCount": 40,195 "type": "git",196 "url": "https://gerrit.delta.rocks/remowt"197 },198 "original": {199 "type": "git",200 "url": "https://gerrit.delta.rocks/remowt"201 }202 },203 "root": {174 "root": {204 "inputs": {175 "inputs": {205 "crane": "crane",176 "crane": "crane",206 "flake-parts": "flake-parts",177 "flake-parts": "flake-parts",207 "fleet-tf": "fleet-tf",178 "fleet-tf": "fleet-tf",208 "nix": "nix",179 "nix": "nix",209 "nixpkgs": "nixpkgs",180 "nixpkgs": "nixpkgs",210 "remowt-agents": "remowt-agents",211 "rust-overlay": "rust-overlay_2",181 "rust-overlay": "rust-overlay",212 "shelly": "shelly",182 "shelly": "shelly",213 "treefmt-nix": "treefmt-nix"183 "treefmt-nix": "treefmt-nix"214 }184 }215 },185 },216 "rust-overlay": {217 "inputs": {218 "nixpkgs": [219 "remowt-agents",220 "nixpkgs"221 ]222 },223 "locked": {224 "lastModified": 1769309768,225 "owner": "oxalica",226 "repo": "rust-overlay",227 "rev": "140c9dc582cb73ada2d63a2180524fcaa744fad5",228 "type": "github"229 },230 "original": {231 "owner": "oxalica",232 "repo": "rust-overlay",233 "type": "github"234 }235 },236 "rust-overlay_2": {186 "rust-overlay": {237 "inputs": {187 "inputs": {238 "nixpkgs": [188 "nixpkgs": [239 "nixpkgs"189 "nixpkgs"flake.nixdiffbeforeafterboth23 url = "github:numtide/treefmt-nix";23 url = "github:numtide/treefmt-nix";24 inputs.nixpkgs.follows = "nixpkgs";24 inputs.nixpkgs.follows = "nixpkgs";25 };25 };26 remowt-agents = {27 url = "git+https://gerrit.delta.rocks/remowt";28 inputs.nixpkgs.follows = "nixpkgs";29 inputs.flake-parts.follows = "flake-parts";30 inputs.crane.follows = "crane";31 inputs.shelly.follows = "shelly";32 };33 # DeterminateSystem's nix fork is controversial, but I don't mind it,26 # DeterminateSystem's nix fork is controversial, but I don't mind it,34 # and it has lazy-trees support which is useful for fleet.27 # and it has lazy-trees support which is useful for fleet.35 nix = {28 nix = {pkgs/default.nixdiffbeforeafterboth3 craneLib,3 craneLib,4 inputs,4 inputs,5}:5}:6{6let7 fleet = callPackage ./fleet.nix { inherit craneLib inputs; };7 remowt-agents-bundle = callPackage ./remowt-agents-bundle.nix { inherit craneLib; };8in9{8 remowt-plugin-fleet = callPackage ./remowt-plugin-fleet.nix { inherit craneLib inputs; };10 fleet = callPackage ./fleet.nix { inherit craneLib inputs; };9 fleet-install-secrets = callPackage ./fleet-install-secrets.nix { inherit craneLib; };11 fleet-install-secrets = callPackage ./fleet-install-secrets.nix { inherit craneLib; };10 fleet-generator-helper = callPackage ./fleet-generator-helper.nix { inherit craneLib; };12 fleet-generator-helper = callPackage ./fleet-generator-helper.nix { inherit craneLib; };11}1314 inherit remowt-agents-bundle;15 remowt-plugin-fleet = callPackage ./remowt-plugin-fleet.nix {16 inherit craneLib inputs remowt-agents-bundle;17 };18 remowt-ssh = callPackage ./remowt-ssh.nix { inherit craneLib remowt-agents-bundle; };19}1220pkgs/fleet.nixdiffbeforeafterboth3 craneLib,3 craneLib,4 installShellFiles,4 installShellFiles,5 inputs,5 inputs,6 remowt-agents-bundle,677 stdenv,8 stdenv,8 pkg-config,9 pkg-config,262727 cargoExtraArgs = "--locked -p ${pname}";28 cargoExtraArgs = "--locked -p ${pname}";282929 REMOWT_AGENTS_DIR = "${inputs.remowt-agents.packages.${system}.remowt-agents}";30 REMOWT_AGENTS_DIR = "${remowt-agents-bundle}";30 # TODO: built-in fleet prompter should be a prodash widget, or it should require31 # TODO: built-in fleet prompter should be a prodash widget, or it should require31 # tty remowt prompter running on host machine idk.32 # tty remowt prompter running on host machine idk.32 ROFI = "${rofi}/bin/rofi";33 ROFI = "${rofi}/bin/rofi";pkgs/remowt-agents-bundle.nixdiffbeforeafterbothno changes
pkgs/remowt-ssh.nixdiffbeforeafterbothno changes
remowt/cmds/remowt-agent/Cargo.tomldiffbeforeafterboth2name = "remowt-agent"2name = "remowt-agent"3description = "remowt on-host agent serving fs/pty/systemd endpoints over bifrostlink"3description = "remowt on-host agent serving fs/pty/systemd endpoints over bifrostlink"4version.workspace = true4version.workspace = true5edition = "2021"5edition.workspace = true6license.workspace = true6license.workspace = true778[dependencies]8[dependencies]remowt/cmds/remowt-agent/src/askpass.rsdiffbeforeafterboth1use std::borrow::Cow;1use std::borrow::Cow;2use std::io::Write as _;2use std::io::Write as _;334use anyhow::Context as _;4use bifrostlink::declarative::RemoteEndpoints as _;5use remowt_link_shared::{gateway, Address, BifConfig};5use remowt_ui_prompt::dbus::{DbusPrompterInterface, DbusPrompterProxy, BUS_NAME, PROMPTER_PATH};6use remowt_ui_prompt::bifrost::PromptEndpointsClient;6use remowt_ui_prompt::{Prompter, Source};7use remowt_ui_prompt::{Prompter, Source};7use tracing::debug;8use zbus::Connection;910pub async fn serve<P>(conn: &Connection, prompter: P) -> anyhow::Result<()>11where12 P: Prompter + 'static,13{14 conn.object_server()15 .at(PROMPTER_PATH, DbusPrompterInterface(prompter))16 .await?;17 match conn.request_name(BUS_NAME).await {18 Ok(()) => {}19 Err(zbus::Error::NameTaken) => {20 debug!("{BUS_NAME} already owned, chaining to upstream");21 }22 Err(e) => return Err(e.into()),23 }24 Ok(())25}26827pub async fn ask(prompt: &str, description: String) -> anyhow::Result<()> {9pub async fn ask(prompt: &str, description: String) -> anyhow::Result<()> {28 let conn = Connection::session()10 let rpc = gateway::connect(&gateway::socket_path()?).await?;29 .await30 .context("connecting to the session bus (DBUS_SESSION_BUS_ADDRESS)")?;31 let proxy = DbusPrompterProxy::builder(&conn)11 let prompter = PromptEndpointsClient::<BifConfig>::wrap(rpc.remote(Address::User));32 .destination(BUS_NAME)?33 .path(PROMPTER_PATH)?34 .build()35 .await?;3637 let password = proxy12 let password = Prompter::prompt_text(38 .prompt_text(13 &prompter,39 false,14 false,40 prompt,15 prompt,41 &description,16 &description,remowt/cmds/remowt-agent/src/bus.rsdiffbeforeafterbothno changes
remowt/cmds/remowt-agent/src/editor.rsdiffbeforeafterboth3use std::time::Duration;3use std::time::Duration;4use std::{fs, io};4use std::{fs, io};556use anyhow::{bail, Context as _};6use anyhow::{anyhow, bail, Context as _};7use bifrostlink::declarative::RemoteEndpoints as _;7use nix::libc;8use nix::libc;8use remowt_link_shared::editor::EditorEndpointsClient;9use remowt_link_shared::editor::EditorEndpointsClient;9use tokio::process::Command;10use zbus::{fdo, interface, proxy, Connection};10use remowt_link_shared::{gateway, Address, BifConfig};1112use remowt_link_shared::BifConfig;11use tokio::process::Command;1314const BUS_NAME: &str = "lach.RemowtEditor";15const SERVICE_PATH: &str = "/lach/Editor";1617pub struct EditorService {18 editor: EditorEndpointsClient<BifConfig>,19}2021#[interface(name = "lach.RemowtEditor")]22impl EditorService {23 /// Attach the User's GUI to the nvim server at `socket_path` (on the remote),24 /// blocking until the user is done.25 async fn edit(&self, socket_path: String) -> fdo::Result<()> {26 self.editor27 .open_editor(socket_path)28 .await29 .map_err(|e| fdo::Error::Failed(format!("requesting editor on the User: {e}")))?30 .map_err(|e| fdo::Error::Failed(format!("editor failed: {e}")))?;31 Ok(())32 }3334 async fn forward_tcp(&self, addr: String) -> fdo::Result<u16> {35 let local = self36 .editor37 .expose_tcp(addr)38 .await39 .map_err(|e| fdo::Error::Failed(format!("requesting tcp forward on the User: {e}")))?40 .map_err(|e| fdo::Error::Failed(format!("tcp forward failed: {e}")))?;41 Ok(local)42 }4344 async fn forward_udp(&self, addr: String) -> fdo::Result<u16> {45 let local = self46 .editor47 .expose_udp(addr)48 .await49 .map_err(|e| fdo::Error::Failed(format!("requesting udp forward on the User: {e}")))?50 .map_err(|e| fdo::Error::Failed(format!("udp forward failed: {e}")))?;51 Ok(local)52 }53}5455pub async fn serve(56 conn: &Connection,57 editor: EditorEndpointsClient<BifConfig>,58) -> anyhow::Result<()> {59 conn.object_server()60 .at(SERVICE_PATH, EditorService { editor })61 .await?;62 conn.request_name(BUS_NAME).await?;63 Ok(())64}6566#[proxy(interface = "lach.RemowtEditor")]67trait RemowtEditor {68 async fn edit(&self, socket_path: &str) -> fdo::Result<()>;69 async fn forward_tcp(&self, addr: &str) -> fdo::Result<u16>;70 async fn forward_udp(&self, addr: &str) -> fdo::Result<u16>;71}721273pub async fn forward(udp: bool, addr: String) -> anyhow::Result<()> {13pub async fn forward(udp: bool, addr: String) -> anyhow::Result<()> {74 let conn = Connection::session()14 let rpc = gateway::connect(&gateway::socket_path()?).await?;75 .await76 .context("connecting to the session bus (DBUS_SESSION_BUS_ADDRESS)")?;77 let proxy = RemowtEditorProxy::builder(&conn)15 let editor = EditorEndpointsClient::<BifConfig>::wrap(rpc.remote(Address::User));78 .destination(BUS_NAME)?79 .path(SERVICE_PATH)?80 .build()81 .await?;82 let local = if udp {16 let local = if udp {83 proxy.forward_udp(&addr).await?17 editor.expose_udp(addr).await84 } else {18 } else {85 proxy.forward_tcp(&addr).await?19 editor.expose_tcp(addr).await86 };20 }21 .map_err(|e| anyhow!("requesting forward on the User: {e}"))?22 .map_err(|e| anyhow!("forward failed: {e}"))?;87 println!("{local}");23 println!("{local}");88 Ok(())24 Ok(())89}25}125 .await61 .await126 .context("nvim did not start its server")?;62 .context("nvim did not start its server")?;12763128 let conn = Connection::session()64 let rpc = gateway::connect(&gateway::socket_path()?).await?;129 .await130 .context("connecting to the session bus (DBUS_SESSION_BUS_ADDRESS)")?;131 let proxy = RemowtEditorProxy::builder(&conn)65 let editor = EditorEndpointsClient::<BifConfig>::wrap(rpc.remote(Address::User));132 .destination(BUS_NAME)?133 .path(SERVICE_PATH)?134 .build()135 .await?;136 let result = proxy.edit(&sock_str).await;66 let result = editor67 .open_editor(sock_str)68 .await69 .map_err(|e| anyhow!("requesting editor on the User: {e}"))70 .and_then(|r| r.map_err(|e| anyhow!("editor failed: {e}")));13771138 if tokio::time::timeout(Duration::from_secs(2), child.wait())72 if tokio::time::timeout(Duration::from_secs(2), child.wait())139 .await73 .await143 }77 }144 let _ = fs::remove_file(&sock);78 let _ = fs::remove_file(&sock);14579146 result?;80 result147 Ok(())148}81}14982150async fn wait_for_socket(path: &Path) -> anyhow::Result<()> {83async fn wait_for_socket(path: &Path) -> anyhow::Result<()> {remowt/cmds/remowt-agent/src/main.rsdiffbeforeafterboth7use std::path::PathBuf;7use std::path::PathBuf;8use std::sync::{Arc, Mutex, OnceLock};8use std::sync::{Arc, Mutex, OnceLock};9910use bifrostlink::declarative::RemoteEndpoints;10use bifrostlink::Rpc;11use bifrostlink::Rpc;11use bifrostlink::declarative::RemoteEndpoints;12use bifrostlink_ports::stdio::from_stdio;12use bifrostlink_ports::stdio::from_stdio;13use bifrostlink_ports::unix_socket::from_socket;13use bifrostlink_ports::unix_socket::from_socket;14use clap::Parser;14use clap::Parser;17 subprocess::Subprocess, systemd::Systemd,17 subprocess::Subprocess, systemd::Systemd,18};18};19use remowt_link_shared::iroh_tunnel::TunnelDialer;19use remowt_link_shared::iroh_tunnel::TunnelDialer;20use remowt_link_shared::{editor::EditorEndpointsClient, Address, BifConfig};20use remowt_link_shared::{Address, BifConfig, gateway};21use remowt_polkit_shared::{emphasize, Identity, PidDisplay};21use remowt_polkit_shared::{Identity, PidDisplay, emphasize};22use remowt_ui_prompt::bifrost::PromptEndpointsClient;22use remowt_ui_prompt::bifrost::{PromptEndpointsClient, serve_prompts};23use remowt_ui_prompt::rofi::RofiPrompter;23use remowt_ui_prompt::rofi::RofiPrompter;24use remowt_ui_prompt::{PrependSourcePrompter, Prompter, Source};24use remowt_ui_prompt::{PrependSourcePrompter, Prompter, Source};25use tokio::fs;25use tokio::fs;29use tracing::{debug, trace};29use tracing::{debug, trace};30use zbus::fdo;30use zbus::fdo;31use zbus::zvariant::{OwnedValue, Str};31use zbus::zvariant::{OwnedValue, Str};32use zbus::{interface, Connection};32use zbus::{Connection, interface};33use zbus_polkit::policykit1::Subject;33use zbus_polkit::policykit1::Subject;343435use self::helper::{Helper, SocketHelper, SuidHelper};35use self::helper::{Helper, SocketHelper, SuidHelper};363637pub mod askpass;37pub mod askpass;38pub mod bus;39pub mod editor;38pub mod editor;40pub mod helper;39pub mod helper;4140132 0 => {131 0 => {133 return Err(fdo::Error::AuthFailed(132 return Err(fdo::Error::AuthFailed(134 "no identity to authenticate as".to_owned(),133 "no identity to authenticate as".to_owned(),135 ))134 ));136 }135 }137 1 => 0,136 1 => 0,138 _ => {137 _ => prompter139 prompter140 .prompt_enum(138 .prompt_enum(141 "Identity",139 "Identity",142 "Select identity to use for polkit authorization",140 "Select identity to use for polkit authorization",143 &identity_displays,141 &identity_displays,144 &[],142 &[],145 )143 )146 .await?144 .await147 }145 .map_err(prompt_err)?,148 };146 };149 debug!("identity chosen");147 debug!("identity chosen");150148198 }196 }199}197}198199fn prompt_err(value: remowt_ui_prompt::Error) -> fdo::Error {200 use remowt_ui_prompt::Error;201 match value {202 Error::Cancel => fdo::Error::NoReply("input was cancelled".to_owned()),203 Error::Remote(e) => fdo::Error::NoReply(format!("remote error occured: {e}")),204 Error::InputError(e) => fdo::Error::Failed(e),205 }206}200207201const OBJ_PATH: &str = "/org/freedesktop/PolicyKit1/AuthenticationAgent";208const OBJ_PATH: &str = "/org/freedesktop/PolicyKit1/AuthenticationAgent";202209268 };275 };269 register_auth_agent(&system_conn, Agent::new(helper, RofiPrompter)).await?;276 register_auth_agent(&system_conn, Agent::new(helper, RofiPrompter)).await?;270277271 let session_conn = Connection::session().await?;278 let mut rpc = Rpc::<BifConfig>::new(Address::User);279 serve_prompts(&mut rpc, RofiPrompter);280272 askpass::serve(&session_conn, RofiPrompter).await?;281 gateway::serve(rpc.clone(), &gateway::local_socket()?).await?;273282274 let _keep_alive = (system_conn, session_conn);283 let _keep_alive = (system_conn, rpc);275 pending().await284 pending().await276}285}277async fn main_real_agent(286async fn main_real_agent(298 remowt_plugin::host::serve(&mut rpc);307 remowt_plugin::host::serve(&mut rpc);299308300 let user_prompter = PromptEndpointsClient::wrap(rpc.remote(Address::User));309 let user_prompter = PromptEndpointsClient::wrap(rpc.remote(Address::User));301 let editor_client = EditorEndpointsClient::wrap(rpc.remote(Address::User));302303 let bus = bus::spawn().await?;304 askpass::serve(&bus.conn, user_prompter.clone()).await?;305 editor::serve(&bus.conn, editor_client).await?;306310307 let helpers = tempfile::Builder::new().prefix("remowt-path.").tempdir()?;311 let helpers = tempfile::Builder::new().prefix("remowt-path.").tempdir()?;312 let gateway_socket = helpers.path().join(gateway::SOCKET_NAME);308 let exe = std::env::current_exe()?;313 let exe = std::env::current_exe()?;309 let askpass_helper = helpers.path().join("remowt-askpass");314 let askpass_helper = helpers.path().join("remowt-askpass");310 let editor_helper = helpers.path().join("remowt-editor");315 let editor_helper = helpers.path().join("remowt-editor");342 std::env::set_var("SSH_ASKPASS_REQUIRE", "force");347 std::env::set_var("SSH_ASKPASS_REQUIRE", "force");343 std::env::set_var("EDITOR", &editor_helper);348 std::env::set_var("EDITOR", &editor_helper);344 std::env::set_var("VISUAL", &editor_helper);349 std::env::set_var("VISUAL", &editor_helper);345 std::env::set_var("DBUS_SESSION_BUS_ADDRESS", &bus.address);350 std::env::set_var(gateway::SOCKET_ENV, &gateway_socket);346 }351 }347352348 let port = match path {353 let port = match path {351 };356 };352 rpc.add_direct(Address::User, port, bifrostlink::Rtt(0));357 rpc.add_direct(Address::User, port, bifrostlink::Rtt(0));358359 gateway::serve(rpc.clone(), &gateway_socket).await?;353360354 let polkit_conn = if !privileged && !local {361 let polkit_conn = if !privileged && !local {355 // The unprivileged agent doubles as a polkit authentication agent so356 // `run0` (e.g. our own elevation) routes its prompt to the User over357 // bifrost instead of failing on a tty-less session.358 let conn = Connection::system().await?;362 let conn = Connection::system().await?;359 let helper = SocketHelper {363 let helper = SocketHelper {360 fallback: SuidHelper,364 fallback: SuidHelper,365 None369 None366 };370 };367371368 let _keep_alive = (bus, helpers, polkit_conn);372 let _keep_alive = (helpers, polkit_conn);369 pending().await373 pending().await370}374}371375remowt/cmds/remowt-ssh/Cargo.tomldiffbeforeafterboth2name = "remowt-ssh"2name = "remowt-ssh"3description = "SSH transport client for connecting to a remowt agent"3description = "SSH transport client for connecting to a remowt agent"4version.workspace = true4version.workspace = true5edition = "2021"5edition.workspace = true6license.workspace = true6license.workspace = true778[dependencies]8[dependencies]remowt/cmds/remowt-ssh/src/main.rsdiffbeforeafterboth75 description: "".to_owned(),75 description: "".to_owned(),76 },76 },77 );77 );78 if let Some(sess) = conn.ssh() {78 if conn.ssh().is_some() {79 serve_editor(79 serve_editor(&mut rpc, SshEditor { conn: conn.clone() });80 &mut rpc,81 SshEditor {82 sess,83 conn: conn.clone(),84 },85 );86 }80 }8781remowt/crates/polkit-shared/Cargo.tomldiffbeforeafterboth2name = "remowt-polkit-shared"2name = "remowt-polkit-shared"3description = "Shared polkit/PAM types for remowt"3description = "Shared polkit/PAM types for remowt"4version.workspace = true4version.workspace = true5edition = "2021"5edition.workspace = true6license.workspace = true6license.workspace = true778[dependencies]8[dependencies]remowt/crates/remowt-client/Cargo.tomldiffbeforeafterboth2name = "remowt-client"2name = "remowt-client"3description = "russh-based client connection to a remowt agent"3description = "russh-based client connection to a remowt agent"4version.workspace = true4version.workspace = true5edition = "2021"5edition.workspace = true6license.workspace = true6license.workspace = true778[dependencies]8[dependencies]remowt/crates/remowt-client/src/editor.rsdiffbeforeafterboth6use remowt_endpoints::forward::ForwardClient;6use remowt_endpoints::forward::ForwardClient;7use remowt_link_shared::editor::{EditorBackend, Error};7use remowt_link_shared::editor::{EditorBackend, Error};8use remowt_link_shared::BifConfig;8use remowt_link_shared::BifConfig;9use russh::client::Handle;10use tokio::net::{TcpListener, UdpSocket, UnixListener};9use tokio::net::{TcpListener, UdpSocket, UnixListener};11use tracing::error;10use tracing::error;121113use crate::{Remowt, SshHandler};12use crate::Remowt;141315pub struct SshEditor {14pub struct SshEditor {16 pub sess: Arc<Handle<SshHandler>>,17 pub conn: Remowt,15 pub conn: Remowt,18}16}19impl EditorBackend for SshEditor {17impl EditorBackend for SshEditor {22 let _ = std::fs::remove_file(&local);20 let _ = std::fs::remove_file(&local);23 let listener = UnixListener::bind(&local).map_err(|e| Error::Failed(e.to_string()))?;21 let listener = UnixListener::bind(&local).map_err(|e| Error::Failed(e.to_string()))?;242225 let sess = self.sess.clone();23 let conn = self.conn.clone();26 let forward = tokio::spawn(async move {24 let forward = tokio::spawn(async move {27 loop {25 loop {28 let Ok((mut stream, _)) = listener.accept().await else {26 let Ok((mut stream, _)) = listener.accept().await else {29 break;27 break;30 };28 };31 let sess = sess.clone();29 let conn = conn.clone();32 let remote = socket_path.clone();30 let remote = socket_path.clone();33 tokio::spawn(async move {31 tokio::spawn(async move {32 // Rides the iroh fast tunnel when established, else an33 // ssh-forwarded unix socket.34 let (forwarded, tunnel) = match conn.bind_fast_tunnel("editor", false).await {35 Ok(v) => v,36 Err(e) => {37 error!("editor: bind tunnel failed: {e}");38 return;39 }40 };41 let fclient: ForwardClient<BifConfig> = conn.endpoints();34 match sess.channel_open_direct_streamlocal(remote).await {42 match fclient.connect_unix(tunnel, remote).await {43 Ok(Ok(())) => {}44 Ok(Err(e)) => {45 error!("editor: agent connect_unix failed: {e}");46 return;47 }48 Err(e) => {49 error!("editor: connect_unix rpc failed: {e}");50 return;51 }52 }53 match forwarded.accept().await {35 Ok(ch) => {54 Ok(mut remote) => {36 let mut remote = ch.into_stream();37 let _ = tokio::io::copy_bidirectional(&mut stream, &mut remote).await;55 let _ = tokio::io::copy_bidirectional(&mut stream, &mut remote).await;38 }56 }39 Err(e) => error!("opening direct-streamlocal to nvim failed: {e}"),57 Err(e) => error!("editor: accept tunnel failed: {e}"),40 }58 }41 });59 });42 }60 }remowt/crates/remowt-client/src/lib.rsdiffbeforeafterboth4use std::sync::atomic::AtomicU64;4use std::sync::atomic::AtomicU64;5use std::sync::{Arc, Mutex};5use std::sync::{Arc, Mutex};667use anyhow::{anyhow, bail, ensure, Context as _, Result};7use anyhow::{Context as _, Result, anyhow, bail, ensure};8use bifrostlink::declarative::RemoteEndpoints;8use bifrostlink::declarative::RemoteEndpoints;9use bifrostlink::{Remote, Rpc, Rtt};9use bifrostlink::{Remote, Rpc, Rtt};10use camino::{Utf8Path, Utf8PathBuf};10use camino::{Utf8Path, Utf8PathBuf};11use remowt_link_shared::iroh_tunnel::{DatagramRouter, IrohBiStream, TunnelAddr};11use remowt_link_shared::iroh_tunnel::{DatagramRouter, IrohBiStream, TunnelAddr};12use remowt_link_shared::plugin::PluginEndpointsClient;12use remowt_link_shared::plugin::PluginEndpointsClient;13use remowt_link_shared::port::child_port;13use remowt_link_shared::port::child_port;14use remowt_link_shared::{Address, BifConfig};14use remowt_link_shared::{Address, BifConfig};15use russh::Channel;15use russh::client::{connect, Config, Handle, Handler, Msg, Session};16use russh::client::{Config, Handle, Handler, Msg, Session, connect};16use russh::keys::agent::client::AgentClient;17use russh::keys::agent::AgentIdentity;17use russh::keys::agent::AgentIdentity;18use russh::keys::agent::client::AgentClient;18use russh::keys::check_known_hosts;19use russh::keys::check_known_hosts;19use russh::keys::ssh_key::PublicKey;20use russh::keys::ssh_key::PublicKey;20use russh::Channel;21use tempfile::TempDir;21use tempfile::TempDir;22use tokio::io::AsyncRead;22use tokio::io::AsyncRead;23use tokio::net::UnixListener;23use tokio::net::UnixListener;287 .unwrap_or_else(|| env::var("USER").unwrap_or_else(|_| "root".to_owned()));287 .unwrap_or_else(|| env::var("USER").unwrap_or_else(|_| "root".to_owned()));288288289 let subs: Subs = Arc::new(Mutex::new(HashMap::new()));289 let subs: Subs = Arc::new(Mutex::new(HashMap::new()));290 let config = Config {291 nodelay: true,292 ..Config::default()293 };290 let mut sess = connect(294 let mut sess = connect(291 Arc::new(Config::default()),295 Arc::new(config),292 (hostname.clone(), port),296 (hostname.clone(), port),293 SshHandler {297 SshHandler {294 host: hostname,298 host: hostname,549 }553 }550 }554 }551555552 /// Bind a data tunnel, preferring the iroh fast path when it is up. Escalated tunnels556 /// Bind a data tunnel, preferring the iroh fast path when it is up.553 /// (the privileged agent, a separate process with no iroh connection of its own) and the554 /// local transport always use the ssh/unix path.555 pub async fn bind_fast_tunnel(557 pub async fn bind_fast_tunnel(556 &self,558 &self,557 hint: &str,559 hint: &str,586588587 async fn try_setup_iroh(&self) -> Result<()> {589 async fn try_setup_iroh(&self) -> Result<()> {588 use remowt_endpoints::iroh_tunnel::IrohTunnelClient;590 use remowt_endpoints::iroh_tunnel::IrohTunnelClient;589 use remowt_link_shared::iroh_tunnel::{build_endpoint, ssh_custom_addr, REMOWT_ALPN};591 use remowt_link_shared::iroh_tunnel::{REMOWT_ALPN, build_endpoint, ssh_custom_addr};590592591 let (listener, sock) = self.bind_runtime_unix("iroh-xport").await?;593 let (listener, sock) = self.bind_runtime_unix("iroh-xport").await?;592 let secret = iroh::SecretKey::generate();594 let secret = iroh::SecretKey::generate();605 );607 );606 let conn = ep.connect(addr, REMOWT_ALPN).await?;608 let conn = ep.connect(addr, REMOWT_ALPN).await?;607 ensure!(conn.remote_id() == agent_id, "iroh peer identity mismatch");609 ensure!(conn.remote_id() == agent_id, "iroh peer identity mismatch");608 info!("iroh fast tunnel established");610 debug!("iroh fast tunnel established");609611610 let subs = self.0.iroh.subs.clone();612 let subs = self.0.iroh.subs.clone();611 let accept_conn = conn.clone();613 let accept_conn = conn.clone();637 }639 }638 });640 });639640 let log_conn = conn.clone();641 tokio::spawn(async move {642 tokio::time::sleep(std::time::Duration::from_secs(2)).await;643 for p in log_conn.paths().iter() {644 if p.is_selected() {645 info!(rtt = ?p.rtt(), "iroh selected path: {}", p.remote_addr());646 }647 }648 });649641650 *self.0.iroh.router.lock().expect("lock") = Some(DatagramRouter::spawn(conn.clone()));642 *self.0.iroh.router.lock().expect("lock") = Some(DatagramRouter::spawn(conn.clone()));651 *self.0.iroh.endpoint.lock().expect("lock") = Some(ep);643 *self.0.iroh.endpoint.lock().expect("lock") = Some(ep);688}680}689681690fn local_runtime_dir() -> Result<(Utf8PathBuf, Option<TempDir>)> {682fn local_runtime_dir() -> Result<(Utf8PathBuf, Option<TempDir>)> {691 if let Ok(dir) = env::var("XDG_RUNTIME_DIR") {683 if let Ok(dir) = env::var("XDG_RUNTIME_DIR")692 if !dir.is_empty() {684 && !dir.is_empty()685 {693 return Ok((Utf8PathBuf::from(dir), None));686 return Ok((Utf8PathBuf::from(dir), None));694 }687 }695 }696 let tmp = tempfile::Builder::new()688 let tmp = tempfile::Builder::new()697 .prefix("remowt.")689 .prefix("remowt.")698 .rand_bytes(12)690 .rand_bytes(12)remowt/crates/remowt-endpoints/Cargo.tomldiffbeforeafterboth2name = "remowt-endpoints"2name = "remowt-endpoints"3description = "Nix daemon proxy"3description = "Nix daemon proxy"4version.workspace = true4version.workspace = true5edition = "2021"5edition.workspace = true6license.workspace = true6license.workspace = true778[dependencies]8[dependencies]remowt/crates/remowt-endpoints/src/forward.rsdiffbeforeafterboth3use std::sync::atomic::{AtomicU64, Ordering};3use std::sync::atomic::{AtomicU64, Ordering};4use std::sync::{Arc, Mutex};4use std::sync::{Arc, Mutex};556use bifrostlink::declarative::endpoints;6use bifrostlink::Config;7use bifrostlink::Config;7use bifrostlink::declarative::endpoints;8use remowt_link_shared::iroh_tunnel::{TunnelAddr, TunnelDialer};8use remowt_link_shared::iroh_tunnel::{TunnelAddr, TunnelDialer};9use serde::{Deserialize, Serialize};9use serde::{Deserialize, Serialize};10use std::result::Result;10use std::result::Result;11use tokio::net::{TcpStream, UdpSocket};11use tokio::net::{TcpStream, UdpSocket, UnixStream};12use tracing::warn;12use tracing::warn;131314#[derive(Serialize, Deserialize, Debug, thiserror::Error)]14#[derive(Serialize, Deserialize, Debug, thiserror::Error)]108 Ok(session)108 Ok(session)109 }109 }110111 #[endpoints(id = 3)]112 async fn connect_unix(&self, tunnel: TunnelAddr, path: String) -> Result<(), Error> {113 let stream = self114 .dialer115 .connect_tunnel(&tunnel)116 .await117 .map_err(|e| Error::Tunnel(e.to_string()))?;118 let unix = UnixStream::connect(&path)119 .await120 .map_err(|e| Error::Connect(path, e.to_string()))?;121 tokio::spawn(async move {122 let mut stream = stream;123 let mut unix = unix;124 let _ = tokio::io::copy_bidirectional(&mut stream, &mut unix).await;125 });126 Ok(())127 }110}128}111129112fn unspecified_for(target: &SocketAddr) -> SocketAddr {130fn unspecified_for(target: &SocketAddr) -> SocketAddr {remowt/crates/remowt-link-shared/Cargo.tomldiffbeforeafterboth2name = "remowt-link-shared"2name = "remowt-link-shared"3description = "Shared bifrostlink endpoint wiring for remowt"3description = "Shared bifrostlink endpoint wiring for remowt"4version.workspace = true4version.workspace = true5edition = "2021"5edition.workspace = true6license.workspace = true6license.workspace = true778[dependencies]8[dependencies]13serde = { workspace = true, features = ["derive"] }13serde = { workspace = true, features = ["derive"] }14serde_json.workspace = true14serde_json.workspace = true15thiserror.workspace = true15thiserror.workspace = true16tokio = { workspace = true, features = ["fs", "io-util", "macros", "net", "sync", "rt"] }16tokio = { workspace = true, features = ["fs", "io-util", "macros", "net", "rt", "sync"] }17tokio-util = { workspace = true, features = ["codec"] }17tokio-util = { workspace = true, features = ["codec"] }18tracing.workspace = true18tracing.workspace = true19remowt-ui-prompt.workspace = true19uuid = { workspace = true, features = ["v4"] }202021iroh.workspace = true21futures.workspace = true22iroh-base.workspace = true22iroh.workspace = true23noq-udp.workspace = true23iroh-base.workspace = true24n0-watcher.workspace = true24n0-watcher.workspace = true25futures.workspace = true25noq-udp.workspace = true2626remowt/crates/remowt-link-shared/src/lib.rsdiffbeforeafterboth7use serde::{Deserialize, Serialize};7use serde::{Deserialize, Serialize};889pub mod editor;9pub mod editor;10pub mod gateway;10pub mod iroh_tunnel;11pub mod iroh_tunnel;11pub mod port;12pub mod port;121316 Agent,17 Agent,17 AgentPrivileged,18 AgentPrivileged,18 Plugin(u16),19 Plugin(u16),20 Ephemeral(uuid::Uuid),19}21}20impl AddressT for Address {}22impl AddressT for Address {}212328 #[error("response: {0}")]30 #[error("response: {0}")]29 Response(String),31 Response(String),3031 #[error(transparent)]32 Ui(#[from] remowt_ui_prompt::Error),33}32}343335impl From<ListenerForYourRequestHasBeenDeadError> for Error {34impl From<ListenerForYourRequestHasBeenDeadError> for Error {38 }37 }39}38}40impl From<serde_json::Error> for Error {39impl From<serde_json::Error> for Error {41 fn from(_value: serde_json::Error) -> Self {40 fn from(e: serde_json::Error) -> Self {42 Self::ListenerDead41 Self::Response(format!("{e}"))43 }42 }44}43}45impl From<Error> for ResponseError {44impl From<Error> for ResponseError {remowt/crates/remowt-link-shared/src/plugin.rsdiffbeforeafterboth10 BadName,10 BadName,11 #[error("spawning plugin failed: {0}")]11 #[error("spawning plugin failed: {0}")]12 Spawn(String),12 Spawn(String),13 #[error("failed to wait for connection, plugin process has died?")]14 ConnectionWaitError,13 #[error("agent is shutting down")]15 #[error("agent is shutting down")]14 Gone,16 Gone,15}17}remowt/crates/remowt-plugin/Cargo.tomldiffbeforeafterboth2name = "remowt-plugin"2name = "remowt-plugin"3description = "Plugin host and protocol for remowt agents"3description = "Plugin host and protocol for remowt agents"4version.workspace = true4version.workspace = true5edition = "2021"5edition.workspace = true6license.workspace = true6license.workspace = true778[dependencies]8[dependencies]remowt/crates/remowt-plugin/src/host.rsdiffbeforeafterboth8use remowt_link_shared::plugin::{Error, PluginEndpoints, PluginHost};8use remowt_link_shared::plugin::{Error, PluginEndpoints, PluginHost};9use remowt_link_shared::port::child_port;9use remowt_link_shared::port::child_port;10use remowt_link_shared::{Address, BifConfig};10use remowt_link_shared::{Address, BifConfig};11use tokio::select;111212pub fn serve(rpc: &mut Rpc<BifConfig>) {13pub fn serve(rpc: &mut Rpc<BifConfig>) {13 let host = Host {14 let host = Host {25}26}262727impl Host {28impl Host {28 fn spawn(&self, id: u16, path: impl AsRef<OsStr>) -> Result<(), Error> {29 async fn spawn(&self, id: u16, path: impl AsRef<OsStr>) -> Result<(), Error> {29 let rpc = self.rpc.clone().upgrade().ok_or(Error::Gone)?;30 let rpc = self.rpc.clone().upgrade().ok_or(Error::Gone)?;303131 let mut child = Command::new(path)32 let mut child = Command::new(path)40 let stdout = child.stdout.take().expect("stdout piped");41 let stdout = child.stdout.take().expect("stdout piped");414242 let addr = Address::Plugin(id);43 let addr = Address::Plugin(id);43 rpc.add_direct(addr, child_port(stdout, stdin), Rtt(0));44 rpc.add_direct(addr.clone(), child_port(stdout, stdin), Rtt(0));4546 select! {47 e = rpc.wait_for_connection_to(addr) => {48 if e.is_err() {49 return Err(Error::ConnectionWaitError)50 }51 },52 _ = child.wait() => {53 return Err(Error::ConnectionWaitError)54 }55 };44 self.children.lock().expect("not poisoned").push(child);56 self.children.lock().expect("not poisoned").push(child);5745 Ok(())58 Ok(())58 let dir = exe71 let dir = exe59 .parent()72 .parent()60 .ok_or_else(|| Error::Spawn("primary agent has no parent directory".to_owned()))?;73 .ok_or_else(|| Error::Spawn("primary agent has no parent directory".to_owned()))?;61 self.spawn(id, dir.join(&name))74 self.spawn(id, dir.join(&name)).await62 }75 }637664 async fn load_plugin_path(&self, id: u16, path: String) -> Result<(), Error> {77 async fn load_plugin_path(&self, id: u16, path: String) -> Result<(), Error> {65 if path.is_empty() || path.contains('\0') {78 if path.is_empty() || path.contains('\0') {66 return Err(Error::BadName);79 return Err(Error::BadName);67 }80 }68 self.spawn(id, path)81 self.spawn(id, path).await69 }82 }70}83}7184remowt/crates/remowt-ui-prompt/src/bifrost.rsdiffbeforeafterboth103where103where104 P: Prompter + Send + Sync + 'static,104 P: Prompter + Send + Sync + 'static,105 C: Config,105 C: Config,106 C::Error: From<Error>,107{106{108 PromptEndpoints(prompt).register_endpoints(rpc);107 PromptEndpoints(prompt).register_endpoints(rpc);109}108}rust-toolchain.tomldiffbeforeafterboth1[toolchain]1[toolchain]2channel = "stable"2channel = "stable"3components = ["rustfmt", "clippy", "rust-analyzer", "rust-src"]3components = ["rustfmt", "clippy", "rust-analyzer", "rust-src"]4targets = [5 "x86_64-unknown-linux-musl",6 "aarch64-unknown-linux-musl",7 "armv7-unknown-linux-musleabihf",8]49