git.delta.rocks / fleet / refs/commits / 48d120aa4aef

difftreelog

ci use in-tree remowt-agent

qztpqskoYaroslav Bolyukin2026-06-18parent: #0ea4b47.patch.diff

42 files changed

modifiedCargo.lockdiffbeforeafterboth
722722
723[[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]
19071907
5011 "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]
50425043
5043[[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]
51025103
5103[[package]]5104[[package]]
modifiedCargo.tomldiffbeforeafterboth
15remowt-fleet = { path = "./crates/remowt-fleet" }15remowt-fleet = { path = "./crates/remowt-fleet" }
1616
17remowt-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" }
2323
24bifrostlink = "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"
2727
28iroh = { 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"
32
33uuid = { version = "1", features = ["v4"] }
34russh = { version = "0.61.2", default-features = false, features = [31noq-udp = { version = "1.0.0", default-features = false }
35 "ring",32
36 "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/3484
81tokio-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"
9288
93[profile.dev]89[profile.dev]
94panic = "abort"90panic = "abort"
modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
5use 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 }
146147
147 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) => {
modifiedcmds/fleet/src/cmds/rollback.rsdiffbeforeafterboth
4use 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?;
113109
deletedcmds/fleet/src/extra_args.rsdiffbeforeafterboth

no changes

modifiedcmds/fleet/src/main.rsdiffbeforeafterboth
1#![recursion_limit = "512"]1#![recursion_limit = "512"]
22
3pub(crate) mod cmds;3pub(crate) mod cmds;
4// pub(crate) mod command;
5pub(crate) mod extra_args;
64
7use std::{process::ExitCode, sync::Arc};5use std::{process::ExitCode, sync::Arc};
86
modifiedcmds/generator-helper/src/main.rsdiffbeforeafterboth
14use 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 );
339339
340 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();
349349
350 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();
modifiedcmds/remowt-plugin-fleet/Cargo.tomldiffbeforeafterboth
2name = "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 = true
5edition = "2021"5edition.workspace = true
66
7[dependencies]7[dependencies]
8remowt-fleet.workspace = true8remowt-fleet.workspace = true
modifiedcrates/fleet-base/src/deploy.rsdiffbeforeafterboth
11use 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};
1313
14use crate::host::{Config, ConfigHost, DeployKind, Generation, GenerationStorage};14use crate::host::{ConfigHost, DeployKind};
15use crate::pins::{Generation, GenerationStorage};
1516
16#[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_systemd
265 .stop("rollback-watchdog-run.timer".to_owned())266 .stop("rollback-watchdog-run.timer".to_owned())
266 .await267 .await
267 {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_fs
272 .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}
281281
282pub 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,
modifiedcrates/fleet-base/src/fleetdata.rsdiffbeforeafterboth
6 },6 },
7 fmt,7 fmt,
8 io::{self, Cursor},8 io::{self, Cursor},
9 str::FromStr,
9 sync::RwLock,10 sync::RwLock,
10};11};
1112
92 #[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 distributions
295 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_at
298 if expires_at < now {300 && expires_at < now
301 {
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 secrets
305 /// Also see expected_owner_removed307 /// Also see expected_owner_removed
308 #[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(&current_owners);326 let mut to_add = expected_owners.difference(&current_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 }
332336
333 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 it
410 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_owner
414 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-owned
418 // 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 them
419 }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_owner
446 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-owned
450 // 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 them
451 }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}
521523
522struct 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}
570572
571enum 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}
684686
685impl 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 }
689691
713 }715 }
714716
715 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 }
720720
721 pub fn contains(&self, secret: &str) -> bool {721 pub fn contains(&self, secret: &str) -> bool {
modifiedcrates/fleet-base/src/host.rsdiffbeforeafterboth
13use 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};
2827
29use 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>>,
102101
103 // 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 it
104 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>,
107106
114 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}
119118
120const 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}
134133
135#[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}
150
151#[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}
169140
170impl 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 = self
174 .remowt()
175 .await?
176 .plugin_endpoints::<remowt_fleet::NixClient<_>>(plugin_id);
177 let raw = nix
178 .list_generations(profile.to_owned())
179 .await
180 .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 }
201
202 pub fn set_session_destination(&self, dest: String) {142 pub fn set_session_destination(&self, dest: String) {
203 self.session_destination143 self.session_destination
215 .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 fleet
234 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 either
236 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 fleet
238 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 nixos
239 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 possible
244 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().await
286 }223 }
287224
225 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_plugin
305 .run0_load_plugin_path(NIX_PLUGIN_ID, bin.as_str())247 .run0_load_plugin_path(NIX_PLUGIN_ID, bin.as_str())
306 .await248 .await
307 .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 .await
313 .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());
349
410 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 })
modifiedcrates/fleet-base/src/lib.rsdiffbeforeafterboth
3pub mod host;3pub mod host;
4mod keys;4mod keys;
5pub mod opts;5pub mod opts;
6pub mod pins;
6pub mod primops;7pub mod primops;
78
addedcrates/fleet-base/src/pins.rsdiffbeforeafterboth

no changes

modifiedcrates/nixlike/fuzz/Cargo.tomldiffbeforeafterboth
4version = "0.0.0"4version = "0.0.0"
5authors = ["Automatically generated"]5authors = ["Automatically generated"]
6publish = false6publish = false
7edition = "2024"7edition.workspace = true
88
9[package.metadata]9[package.metadata]
10cargo-fuzz = true10cargo-fuzz = true
modifiedcrates/remowt-fleet/Cargo.tomldiffbeforeafterboth
2name = "remowt-fleet"2name = "remowt-fleet"
3description = "Remowt endpoints for fleet"3description = "Remowt endpoints for fleet"
4version.workspace = true4version.workspace = true
5edition = "2021"5edition.workspace = true
66
7[dependencies]7[dependencies]
8bifrostlink.workspace = true8bifrostlink.workspace = true
18tokio = { workspace = true, features = ["io-util", "net", "process", "rt"] }18tokio = { workspace = true, features = ["io-util", "net", "process", "rt"] }
19tracing.workspace = true19tracing.workspace = true
20uuid.workspace = true20uuid.workspace = true
21remowt-link-shared.workspace = true
2122
modifiedcrates/remowt-fleet/src/lib.rsdiffbeforeafterboth
7use 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 nix
103 .serve_store(store.clone(), TunnelAddr::Unix(remote_sock))
104 .await
105 {
103 Ok(Ok(())) => {}106 Ok(Ok(())) => {}
modifiedflake.lockdiffbeforeafterboth
171 "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"
modifiedflake.nixdiffbeforeafterboth
23 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 = {
modifiedpkgs/default.nixdiffbeforeafterboth
3 craneLib,3 craneLib,
4 inputs,4 inputs,
5}:5}:
6{6let
7 fleet = callPackage ./fleet.nix { inherit craneLib inputs; };7 remowt-agents-bundle = callPackage ./remowt-agents-bundle.nix { inherit craneLib; };
8in
9{
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}13
14 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}
1220
modifiedpkgs/fleet.nixdiffbeforeafterboth
3 craneLib,3 craneLib,
4 installShellFiles,4 installShellFiles,
5 inputs,5 inputs,
6 remowt-agents-bundle,
67
7 stdenv,8 stdenv,
8 pkg-config,9 pkg-config,
2627
27 cargoExtraArgs = "--locked -p ${pname}";28 cargoExtraArgs = "--locked -p ${pname}";
2829
29 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 require
31 # 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";
addedpkgs/remowt-agents-bundle.nixdiffbeforeafterboth

no changes

addedpkgs/remowt-ssh.nixdiffbeforeafterboth

no changes

modifiedremowt/cmds/remowt-agent/Cargo.tomldiffbeforeafterboth
2name = "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 = true
5edition = "2021"5edition.workspace = true
6license.workspace = true6license.workspace = true
77
8[dependencies]8[dependencies]
modifiedremowt/cmds/remowt-agent/src/askpass.rsdiffbeforeafterboth
1use std::borrow::Cow;1use std::borrow::Cow;
2use std::io::Write as _;2use std::io::Write as _;
33
4use 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;
9
10pub async fn serve<P>(conn: &Connection, prompter: P) -> anyhow::Result<()>
11where
12 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}
268
27pub 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 .await
30 .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?;
36
37 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,
deletedremowt/cmds/remowt-agent/src/bus.rsdiffbeforeafterboth

no changes

modifiedremowt/cmds/remowt-agent/src/editor.rsdiffbeforeafterboth
3use std::time::Duration;3use std::time::Duration;
4use std::{fs, io};4use std::{fs, io};
55
6use 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};
11
12use remowt_link_shared::BifConfig;11use tokio::process::Command;
13
14const BUS_NAME: &str = "lach.RemowtEditor";
15const SERVICE_PATH: &str = "/lach/Editor";
16
17pub struct EditorService {
18 editor: EditorEndpointsClient<BifConfig>,
19}
20
21#[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.editor
27 .open_editor(socket_path)
28 .await
29 .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 }
33
34 async fn forward_tcp(&self, addr: String) -> fdo::Result<u16> {
35 let local = self
36 .editor
37 .expose_tcp(addr)
38 .await
39 .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 }
43
44 async fn forward_udp(&self, addr: String) -> fdo::Result<u16> {
45 let local = self
46 .editor
47 .expose_udp(addr)
48 .await
49 .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}
54
55pub 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}
65
66#[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}
7212
73pub 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 .await
76 .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).await
84 } else {18 } else {
85 proxy.forward_tcp(&addr).await?19 editor.expose_tcp(addr).await
86 };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 .await
126 .context("nvim did not start its server")?;62 .context("nvim did not start its server")?;
12763
128 let conn = Connection::session()64 let rpc = gateway::connect(&gateway::socket_path()?).await?;
129 .await
130 .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 = editor
67 .open_editor(sock_str)
68 .await
69 .map_err(|e| anyhow!("requesting editor on the User: {e}"))
70 .and_then(|r| r.map_err(|e| anyhow!("editor failed: {e}")));
13771
138 if tokio::time::timeout(Duration::from_secs(2), child.wait())72 if tokio::time::timeout(Duration::from_secs(2), child.wait())
139 .await73 .await
143 }77 }
144 let _ = fs::remove_file(&sock);78 let _ = fs::remove_file(&sock);
14579
146 result?;80 result
147 Ok(())
148}81}
14982
150async fn wait_for_socket(path: &Path) -> anyhow::Result<()> {83async fn wait_for_socket(path: &Path) -> anyhow::Result<()> {
modifiedremowt/cmds/remowt-agent/src/main.rsdiffbeforeafterboth
7use std::path::PathBuf;7use std::path::PathBuf;
8use std::sync::{Arc, Mutex, OnceLock};8use std::sync::{Arc, Mutex, OnceLock};
99
10use 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;
3434
35use self::helper::{Helper, SocketHelper, SuidHelper};35use self::helper::{Helper, SocketHelper, SuidHelper};
3636
37pub mod askpass;37pub mod askpass;
38pub mod bus;
39pub mod editor;38pub mod editor;
40pub mod helper;39pub mod helper;
4140
132 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 _ => prompter
139 prompter
140 .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 .await
147 }145 .map_err(prompt_err)?,
148 };146 };
149 debug!("identity chosen");147 debug!("identity chosen");
150148
198 }196 }
199}197}
198
199fn 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}
200207
201const OBJ_PATH: &str = "/org/freedesktop/PolicyKit1/AuthenticationAgent";208const OBJ_PATH: &str = "/org/freedesktop/PolicyKit1/AuthenticationAgent";
202209
268 };275 };
269 register_auth_agent(&system_conn, Agent::new(helper, RofiPrompter)).await?;276 register_auth_agent(&system_conn, Agent::new(helper, RofiPrompter)).await?;
270277
271 let session_conn = Connection::session().await?;278 let mut rpc = Rpc::<BifConfig>::new(Address::User);
279 serve_prompts(&mut rpc, RofiPrompter);
280
272 askpass::serve(&session_conn, RofiPrompter).await?;281 gateway::serve(rpc.clone(), &gateway::local_socket()?).await?;
273282
274 let _keep_alive = (system_conn, session_conn);283 let _keep_alive = (system_conn, rpc);
275 pending().await284 pending().await
276}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);
299308
300 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));
302
303 let bus = bus::spawn().await?;
304 askpass::serve(&bus.conn, user_prompter.clone()).await?;
305 editor::serve(&bus.conn, editor_client).await?;
306310
307 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 }
347352
348 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));
358
359 gateway::serve(rpc.clone(), &gateway_socket).await?;
353360
354 let polkit_conn = if !privileged && !local {361 let polkit_conn = if !privileged && !local {
355 // The unprivileged agent doubles as a polkit authentication agent so
356 // `run0` (e.g. our own elevation) routes its prompt to the User over
357 // 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 None
366 };370 };
367371
368 let _keep_alive = (bus, helpers, polkit_conn);372 let _keep_alive = (helpers, polkit_conn);
369 pending().await373 pending().await
370}374}
371375
modifiedremowt/cmds/remowt-ssh/Cargo.tomldiffbeforeafterboth
2name = "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 = true
5edition = "2021"5edition.workspace = true
6license.workspace = true6license.workspace = true
77
8[dependencies]8[dependencies]
modifiedremowt/cmds/remowt-ssh/src/main.rsdiffbeforeafterboth
75 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 }
8781
modifiedremowt/crates/polkit-shared/Cargo.tomldiffbeforeafterboth
2name = "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 = true
5edition = "2021"5edition.workspace = true
6license.workspace = true6license.workspace = true
77
8[dependencies]8[dependencies]
modifiedremowt/crates/remowt-client/Cargo.tomldiffbeforeafterboth
2name = "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 = true
5edition = "2021"5edition.workspace = true
6license.workspace = true6license.workspace = true
77
8[dependencies]8[dependencies]
modifiedremowt/crates/remowt-client/src/editor.rsdiffbeforeafterboth
6use 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;
1211
13use crate::{Remowt, SshHandler};12use crate::Remowt;
1413
15pub 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()))?;
2422
25 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 an
33 // 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 }
modifiedremowt/crates/remowt-client/src/lib.rsdiffbeforeafterboth
4use std::sync::atomic::AtomicU64;4use std::sync::atomic::AtomicU64;
5use std::sync::{Arc, Mutex};5use std::sync::{Arc, Mutex};
66
7use 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()));
288288
289 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 }
551555
552 /// 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 the
554 /// 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,
586588
587 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};
590592
591 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");
609611
610 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 });
639
640 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 });
649641
650 *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}
689681
690fn 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)
modifiedremowt/crates/remowt-endpoints/Cargo.tomldiffbeforeafterboth
2name = "remowt-endpoints"2name = "remowt-endpoints"
3description = "Nix daemon proxy"3description = "Nix daemon proxy"
4version.workspace = true4version.workspace = true
5edition = "2021"5edition.workspace = true
6license.workspace = true6license.workspace = true
77
8[dependencies]8[dependencies]
modifiedremowt/crates/remowt-endpoints/src/forward.rsdiffbeforeafterboth
3use std::sync::atomic::{AtomicU64, Ordering};3use std::sync::atomic::{AtomicU64, Ordering};
4use std::sync::{Arc, Mutex};4use std::sync::{Arc, Mutex};
55
6use 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;
1313
14#[derive(Serialize, Deserialize, Debug, thiserror::Error)]14#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
108 Ok(session)108 Ok(session)
109 }109 }
110
111 #[endpoints(id = 3)]
112 async fn connect_unix(&self, tunnel: TunnelAddr, path: String) -> Result<(), Error> {
113 let stream = self
114 .dialer
115 .connect_tunnel(&tunnel)
116 .await
117 .map_err(|e| Error::Tunnel(e.to_string()))?;
118 let unix = UnixStream::connect(&path)
119 .await
120 .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}
111129
112fn unspecified_for(target: &SocketAddr) -> SocketAddr {130fn unspecified_for(target: &SocketAddr) -> SocketAddr {
modifiedremowt/crates/remowt-link-shared/Cargo.tomldiffbeforeafterboth
2name = "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 = true
5edition = "2021"5edition.workspace = true
6license.workspace = true6license.workspace = true
77
8[dependencies]8[dependencies]
13serde = { workspace = true, features = ["derive"] }13serde = { workspace = true, features = ["derive"] }
14serde_json.workspace = true14serde_json.workspace = true
15thiserror.workspace = true15thiserror.workspace = true
16tokio = { 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 = true
19remowt-ui-prompt.workspace = true19uuid = { workspace = true, features = ["v4"] }
2020
21iroh.workspace = true21futures.workspace = true
22iroh-base.workspace = true22iroh.workspace = true
23noq-udp.workspace = true23iroh-base.workspace = true
24n0-watcher.workspace = true24n0-watcher.workspace = true
25futures.workspace = true25noq-udp.workspace = true
2626
modifiedremowt/crates/remowt-link-shared/src/lib.rsdiffbeforeafterboth
7use serde::{Deserialize, Serialize};7use serde::{Deserialize, Serialize};
88
9pub mod editor;9pub mod editor;
10pub mod gateway;
10pub mod iroh_tunnel;11pub mod iroh_tunnel;
11pub mod port;12pub mod port;
1213
16 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 {}
2123
28 #[error("response: {0}")]30 #[error("response: {0}")]
29 Response(String),31 Response(String),
30
31 #[error(transparent)]
32 Ui(#[from] remowt_ui_prompt::Error),
33}32}
3433
35impl 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 {
modifiedremowt/crates/remowt-link-shared/src/plugin.rsdiffbeforeafterboth
10 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}
modifiedremowt/crates/remowt-plugin/Cargo.tomldiffbeforeafterboth
2name = "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 = true
5edition = "2021"5edition.workspace = true
6license.workspace = true6license.workspace = true
77
8[dependencies]8[dependencies]
modifiedremowt/crates/remowt-plugin/src/host.rsdiffbeforeafterboth
8use 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;
1112
12pub fn serve(rpc: &mut Rpc<BifConfig>) {13pub fn serve(rpc: &mut Rpc<BifConfig>) {
13 let host = Host {14 let host = Host {
25}26}
2627
27impl 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)?;
3031
31 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");
4142
42 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));
45
46 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);
57
45 Ok(())58 Ok(())
58 let dir = exe71 let dir = exe
59 .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)).await
62 }75 }
6376
64 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).await
69 }82 }
70}83}
7184
modifiedremowt/crates/remowt-ui-prompt/src/bifrost.rsdiffbeforeafterboth
103where103where
104 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}
modifiedrust-toolchain.tomldiffbeforeafterboth
1[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