difftreelog
refactor join with remowt monorepo
57 files changed
Cargo.lockdiffbeforeafterboth202021[[package]]21[[package]]22name = "aead"22name = "aead"23version = "0.6.0"23version = "0.6.1"24source = "registry+https://github.com/rust-lang/crates.io-index"24source = "registry+https://github.com/rust-lang/crates.io-index"25checksum = "ef60ac202874e574ce7a7158cc8bca7313dd344322482e4fadee288bf4a306b8"25checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99"26dependencies = [26dependencies = [27 "crypto-common 0.2.2",27 "crypto-common 0.2.2",28 "inout 0.2.2",28 "inout 0.2.2",71source = "registry+https://github.com/rust-lang/crates.io-index"71source = "registry+https://github.com/rust-lang/crates.io-index"72checksum = "da8c919c118108f144adecad74b425b804ad075580d605d9b33c2d6d1c62a2f8"72checksum = "da8c919c118108f144adecad74b425b804ad075580d605d9b33c2d6d1c62a2f8"73dependencies = [73dependencies = [74 "aead 0.6.0",74 "aead 0.6.1",75 "aes 0.9.1",75 "aes 0.9.1",76 "cipher 0.5.2",76 "cipher 0.5.2",77 "ctr 0.10.1",77 "ctr 0.10.1",144 "memchr",144 "memchr",145]145]146147[[package]]148name = "allocator-api2"149version = "0.2.21"150source = "registry+https://github.com/rust-lang/crates.io-index"151checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"146152147[[package]]153[[package]]148name = "android_system_properties"154name = "android_system_properties"230 "password-hash",236 "password-hash",231]237]238239[[package]]240name = "arrayref"241version = "0.3.9"242source = "registry+https://github.com/rust-lang/crates.io-index"243checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"232244233[[package]]245[[package]]234name = "arrayvec"246name = "arrayvec"329dependencies = [341dependencies = [330 "proc-macro2",342 "proc-macro2",331 "quote",343 "quote",332 "syn 2.0.117",344 "syn 2.0.118",333]345]334346335[[package]]347[[package]]364dependencies = [376dependencies = [365 "proc-macro2",377 "proc-macro2",366 "quote",378 "quote",367 "syn 2.0.117",379 "syn 2.0.118",368]380]369381370[[package]]382[[package]]376 "paste",388 "paste",377]389]390391[[package]]392name = "async_io_stream"393version = "0.3.3"394source = "registry+https://github.com/rust-lang/crates.io-index"395checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c"396dependencies = [397 "futures",398 "pharos",399 "rustc_version",400]378401379[[package]]402[[package]]380name = "atomic-waker"403name = "atomic-waker"381version = "1.1.2"404version = "1.1.2"382source = "registry+https://github.com/rust-lang/crates.io-index"405source = "registry+https://github.com/rust-lang/crates.io-index"383checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"406checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"407408[[package]]409name = "attohttpc"410version = "0.30.1"411source = "registry+https://github.com/rust-lang/crates.io-index"412checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9"413dependencies = [414 "base64 0.22.1",415 "http",416 "log",417 "url",418]384419385[[package]]420[[package]]386name = "autocfg"421name = "autocfg"387version = "1.5.1"422version = "1.5.1"388source = "registry+https://github.com/rust-lang/crates.io-index"423source = "registry+https://github.com/rust-lang/crates.io-index"389checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"424checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"425426[[package]]427name = "aws-lc-rs"428version = "1.17.0"429source = "registry+https://github.com/rust-lang/crates.io-index"430checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00"431dependencies = [432 "aws-lc-sys",433 "zeroize",434]435436[[package]]437name = "aws-lc-sys"438version = "0.41.0"439source = "registry+https://github.com/rust-lang/crates.io-index"440checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4"441dependencies = [442 "cc",443 "cmake",444 "dunce",445 "fs_extra",446]447448[[package]]449name = "backon"450version = "1.6.0"451source = "registry+https://github.com/rust-lang/crates.io-index"452checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef"453dependencies = [454 "fastrand",455 "gloo-timers",456 "tokio",457]390458391[[package]]459[[package]]392name = "base16ct"460name = "base16ct"475dependencies = [543dependencies = [476 "proc-macro2",544 "proc-macro2",477 "quote",545 "quote",478 "syn 2.0.117",546 "syn 2.0.118",479]547]480548481[[package]]549[[package]]490 "tracing",558 "tracing",491]559]560561[[package]]562name = "bindgen"563version = "0.69.5"564source = "registry+https://github.com/rust-lang/crates.io-index"565checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"566dependencies = [567 "bitflags 2.13.0",568 "cexpr",569 "clang-sys",570 "itertools 0.12.1",571 "lazy_static",572 "lazycell",573 "proc-macro2",574 "quote",575 "regex",576 "rustc-hash 1.1.0",577 "shlex 1.3.0",578 "syn 2.0.118",579]492580493[[package]]581[[package]]494name = "bindgen"582name = "bindgen"495version = "0.72.1"583version = "0.72.1"496source = "registry+https://github.com/rust-lang/crates.io-index"584source = "registry+https://github.com/rust-lang/crates.io-index"497checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"585checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"498dependencies = [586dependencies = [499 "bitflags",587 "bitflags 2.13.0",500 "cexpr",588 "cexpr",501 "clang-sys",589 "clang-sys",502 "itertools 0.13.0",590 "itertools 0.13.0",507 "regex",595 "regex",508 "rustc-hash 2.1.2",596 "rustc-hash 2.1.2",509 "shlex 1.3.0",597 "shlex 1.3.0",510 "syn 2.0.117",598 "syn 2.0.118",511]599]600601[[package]]602name = "bitflags"603version = "1.3.2"604source = "registry+https://github.com/rust-lang/crates.io-index"605checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"512606513[[package]]607[[package]]514name = "bitflags"608name = "bitflags"525 "digest 0.11.3",619 "digest 0.11.3",526]620]621622[[package]]623name = "blake3"624version = "1.8.5"625source = "registry+https://github.com/rust-lang/crates.io-index"626checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"627dependencies = [628 "arrayref",629 "arrayvec",630 "cc",631 "cfg-if",632 "constant_time_eq",633 "cpufeatures 0.3.0",634]527635528[[package]]636[[package]]529name = "block-buffer"637name = "block-buffer"562 "hybrid-array",670 "hybrid-array",563]671]672673[[package]]674name = "block2"675version = "0.6.2"676source = "registry+https://github.com/rust-lang/crates.io-index"677checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"678dependencies = [679 "objc2",680]564681565[[package]]682[[package]]566name = "blocking"683name = "blocking"625742626[[package]]743[[package]]627name = "bytes"744name = "bytes"628version = "1.11.1"745version = "1.12.0"629source = "registry+https://github.com/rust-lang/crates.io-index"746source = "registry+https://github.com/rust-lang/crates.io-index"630checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"747checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"631748632[[package]]749[[package]]633name = "camino"750name = "camino"663checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f"780checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f"664dependencies = [781dependencies = [665 "find-msvc-tools",782 "find-msvc-tools",783 "jobserver",784 "libc",666 "shlex 2.0.1",785 "shlex 2.0.1",667]786]787788[[package]]789name = "cesu8"790version = "1.1.0"791source = "registry+https://github.com/rust-lang/crates.io-index"792checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"668793669[[package]]794[[package]]670name = "cexpr"795name = "cexpr"815 "heck",940 "heck",816 "proc-macro2",941 "proc-macro2",817 "quote",942 "quote",818 "syn 2.0.117",943 "syn 2.0.118",819]944]820945821[[package]]946[[package]]824source = "registry+https://github.com/rust-lang/crates.io-index"949source = "registry+https://github.com/rust-lang/crates.io-index"825checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"950checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"951952[[package]]953name = "cmake"954version = "0.1.58"955source = "registry+https://github.com/rust-lang/crates.io-index"956checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"957dependencies = [958 "cc",959]826960827[[package]]961[[package]]828name = "cmov"962name = "cmov"829version = "0.5.4"963version = "0.5.4"830source = "registry+https://github.com/rust-lang/crates.io-index"964source = "registry+https://github.com/rust-lang/crates.io-index"831checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"965checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"966967[[package]]968name = "cobs"969version = "0.3.0"970source = "registry+https://github.com/rust-lang/crates.io-index"971checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"972dependencies = [973 "thiserror 2.0.18",974]832975833[[package]]976[[package]]834name = "codespan-reporting"977name = "codespan-reporting"847source = "registry+https://github.com/rust-lang/crates.io-index"990source = "registry+https://github.com/rust-lang/crates.io-index"848checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"991checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"992993[[package]]994name = "combine"995version = "4.6.7"996source = "registry+https://github.com/rust-lang/crates.io-index"997checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"998dependencies = [999 "bytes",1000 "memchr",1001]8491002850[[package]]1003[[package]]851name = "concurrent-queue"1004name = "concurrent-queue"892source = "registry+https://github.com/rust-lang/crates.io-index"1045source = "registry+https://github.com/rust-lang/crates.io-index"893checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"1046checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"10471048[[package]]1049name = "constant_time_eq"1050version = "0.4.2"1051source = "registry+https://github.com/rust-lang/crates.io-index"1052checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"10531054[[package]]1055name = "convert_case"1056version = "0.10.0"1057source = "registry+https://github.com/rust-lang/crates.io-index"1058checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"1059dependencies = [1060 "unicode-segmentation",1061]8941062895[[package]]1063[[package]]896name = "cookie-factory"1064name = "cookie-factory"901 "futures",1069 "futures",902]1070]10711072[[package]]1073name = "cordyceps"1074version = "0.3.4"1075source = "registry+https://github.com/rust-lang/crates.io-index"1076checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a"1077dependencies = [1078 "loom",1079 "tracing",1080]10811082[[package]]1083name = "core-foundation"1084version = "0.9.4"1085source = "registry+https://github.com/rust-lang/crates.io-index"1086checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"1087dependencies = [1088 "core-foundation-sys",1089 "libc",1090]9031091904[[package]]1092[[package]]905name = "core-foundation"1093name = "core-foundation"950 "cfg-if",1138 "cfg-if",951]1139]11401141[[package]]1142name = "critical-section"1143version = "1.2.0"1144source = "registry+https://github.com/rust-lang/crates.io-index"1145checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"11461147[[package]]1148name = "crossbeam-channel"1149version = "0.5.15"1150source = "registry+https://github.com/rust-lang/crates.io-index"1151checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"1152dependencies = [1153 "crossbeam-utils",1154]11551156[[package]]1157name = "crossbeam-epoch"1158version = "0.9.18"1159source = "registry+https://github.com/rust-lang/crates.io-index"1160checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"1161dependencies = [1162 "crossbeam-utils",1163]9521164953[[package]]1165[[package]]954name = "crossbeam-utils"1166name = "crossbeam-utils"9581170959[[package]]1171[[package]]960name = "crypto-bigint"1172name = "crypto-bigint"961version = "0.7.3"1173version = "0.7.4"962source = "registry+https://github.com/rust-lang/crates.io-index"1174source = "registry+https://github.com/rust-lang/crates.io-index"963checksum = "42a0d26b245348befa0c121944541476763dcc46ede886c88f9d12e1697d27c3"1175checksum = "97bb4a855e3b10f84c4e7e895a7de01db7f9a7b7eb7f73ed9773fd52ac686451"964dependencies = [1176dependencies = [965 "cpubits",1177 "cpubits",966 "ctutils",1178 "ctutils",967 "getrandom 0.4.2",1179 "getrandom 0.4.3",968 "hybrid-array",1180 "hybrid-array",969 "num-traits",1181 "num-traits",970 "rand_core 0.10.1",1182 "rand_core 0.10.1",990source = "registry+https://github.com/rust-lang/crates.io-index"1202source = "registry+https://github.com/rust-lang/crates.io-index"991checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"1203checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"992dependencies = [1204dependencies = [993 "getrandom 0.4.2",1205 "getrandom 0.4.3",994 "hybrid-array",1206 "hybrid-array",995 "rand_core 0.10.1",1207 "rand_core 0.10.1",996]1208]1048 "cfg-if",1260 "cfg-if",1049 "cpufeatures 0.2.17",1261 "cpufeatures 0.2.17",1050 "curve25519-dalek-derive",1262 "curve25519-dalek-derive",1051 "digest 0.10.7",1052 "fiat-crypto 0.2.9",1263 "fiat-crypto 0.2.9",1053 "rustc_version",1264 "rustc_version",1054 "subtle",1265 "subtle",1066 "curve25519-dalek-derive",1277 "curve25519-dalek-derive",1067 "digest 0.11.3",1278 "digest 0.11.3",1068 "fiat-crypto 0.3.0",1279 "fiat-crypto 0.3.0",1280 "rand_core 0.10.1",1069 "rustc_version",1281 "rustc_version",1282 "serde",1070 "subtle",1283 "subtle",1071 "zeroize",1284 "zeroize",1072]1285]1079dependencies = [1292dependencies = [1080 "proc-macro2",1293 "proc-macro2",1081 "quote",1294 "quote",1082 "syn 2.0.117",1295 "syn 2.0.118",1083]1296]108412971085[[package]]1298[[package]]1093 "cxxbridge-cmd",1306 "cxxbridge-cmd",1094 "cxxbridge-flags",1307 "cxxbridge-flags",1095 "cxxbridge-macro",1308 "cxxbridge-macro",1096 "foldhash 0.2.0",1309 "foldhash",1097 "link-cplusplus",1310 "link-cplusplus",1098]1311]109913121109 "proc-macro2",1322 "proc-macro2",1110 "quote",1323 "quote",1111 "scratch",1324 "scratch",1112 "syn 2.0.117",1325 "syn 2.0.118",1113]1326]111413271115[[package]]1328[[package]]1123 "indexmap",1336 "indexmap",1124 "proc-macro2",1337 "proc-macro2",1125 "quote",1338 "quote",1126 "syn 2.0.117",1339 "syn 2.0.118",1127]1340]112813411129[[package]]1342[[package]]1141 "indexmap",1354 "indexmap",1142 "proc-macro2",1355 "proc-macro2",1143 "quote",1356 "quote",1144 "syn 2.0.117",1357 "syn 2.0.118",1145]1358]13591360[[package]]1361name = "darling"1362version = "0.20.11"1363source = "registry+https://github.com/rust-lang/crates.io-index"1364checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"1365dependencies = [1366 "darling_core",1367 "darling_macro",1368]13691370[[package]]1371name = "darling_core"1372version = "0.20.11"1373source = "registry+https://github.com/rust-lang/crates.io-index"1374checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"1375dependencies = [1376 "fnv",1377 "ident_case",1378 "proc-macro2",1379 "quote",1380 "strsim",1381 "syn 2.0.118",1382]13831384[[package]]1385name = "darling_macro"1386version = "0.20.11"1387source = "registry+https://github.com/rust-lang/crates.io-index"1388checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"1389dependencies = [1390 "darling_core",1391 "quote",1392 "syn 2.0.118",1393]114613941147[[package]]1395[[package]]1148name = "data-encoding"1396name = "data-encoding"1149version = "2.11.0"1397version = "2.11.0"1150source = "registry+https://github.com/rust-lang/crates.io-index"1398source = "registry+https://github.com/rust-lang/crates.io-index"1151checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"1399checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"14001401[[package]]1402name = "data-encoding-macro"1403version = "0.1.20"1404source = "registry+https://github.com/rust-lang/crates.io-index"1405checksum = "3259c913752a86488b501ed8680446a5ed2d5aeac6e596cb23ba3800768ea32c"1406dependencies = [1407 "data-encoding",1408 "data-encoding-macro-internal",1409]14101411[[package]]1412name = "data-encoding-macro-internal"1413version = "0.1.18"1414source = "registry+https://github.com/rust-lang/crates.io-index"1415checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090"1416dependencies = [1417 "data-encoding",1418 "syn 1.0.109",1419]115214201153[[package]]1421[[package]]1154name = "delegate"1422name = "delegate"1158dependencies = [1426dependencies = [1159 "proc-macro2",1427 "proc-macro2",1160 "quote",1428 "quote",1161 "syn 2.0.117",1429 "syn 2.0.118",1162]1430]116314311164[[package]]1432[[package]]1199 "syn 1.0.109",1467 "syn 1.0.109",1200]1468]14691470[[package]]1471name = "derive_builder"1472version = "0.20.2"1473source = "registry+https://github.com/rust-lang/crates.io-index"1474checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"1475dependencies = [1476 "derive_builder_macro",1477]14781479[[package]]1480name = "derive_builder_core"1481version = "0.20.2"1482source = "registry+https://github.com/rust-lang/crates.io-index"1483checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"1484dependencies = [1485 "darling",1486 "proc-macro2",1487 "quote",1488 "syn 2.0.118",1489]14901491[[package]]1492name = "derive_builder_macro"1493version = "0.20.2"1494source = "registry+https://github.com/rust-lang/crates.io-index"1495checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"1496dependencies = [1497 "derive_builder_core",1498 "syn 2.0.118",1499]15001501[[package]]1502name = "derive_more"1503version = "2.1.1"1504source = "registry+https://github.com/rust-lang/crates.io-index"1505checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"1506dependencies = [1507 "derive_more-impl",1508]15091510[[package]]1511name = "derive_more-impl"1512version = "2.1.1"1513source = "registry+https://github.com/rust-lang/crates.io-index"1514checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"1515dependencies = [1516 "convert_case",1517 "proc-macro2",1518 "quote",1519 "rustc_version",1520 "syn 2.0.118",1521 "unicode-xid",1522]120115231202[[package]]1524[[package]]1203name = "des"1525name = "des"1208 "cipher 0.5.2",1530 "cipher 0.5.2",1209]1531]15321533[[package]]1534name = "diatomic-waker"1535version = "0.2.3"1536source = "registry+https://github.com/rust-lang/crates.io-index"1537checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c"121015381211[[package]]1539[[package]]1212name = "digest"1540name = "digest"1232 "ctutils",1560 "ctutils",1233]1561]15621563[[package]]1564name = "dispatch2"1565version = "0.3.1"1566source = "registry+https://github.com/rust-lang/crates.io-index"1567checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"1568dependencies = [1569 "bitflags 2.13.0",1570 "block2",1571 "libc",1572 "objc2",1573]123415741235[[package]]1575[[package]]1236name = "displaydoc"1576name = "displaydoc"1240dependencies = [1580dependencies = [1241 "proc-macro2",1581 "proc-macro2",1242 "quote",1582 "quote",1243 "syn 2.0.117",1583 "syn 2.0.118",1244]1584]15851586[[package]]1587name = "dlopen2"1588version = "0.8.2"1589source = "registry+https://github.com/rust-lang/crates.io-index"1590checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4"1591dependencies = [1592 "libc",1593 "once_cell",1594 "winapi",1595]15961597[[package]]1598name = "dunce"1599version = "1.0.5"1600source = "registry+https://github.com/rust-lang/crates.io-index"1601checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"124516021246[[package]]1603[[package]]1247name = "ecdsa"1604name = "ecdsa"1258 "zeroize",1615 "zeroize",1259]1616]12601261[[package]]1262name = "ed25519"1263version = "2.2.3"1264source = "registry+https://github.com/rust-lang/crates.io-index"1265checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"1266dependencies = [1267 "pkcs8 0.10.2",1268 "signature 2.2.0",1269]127016171271[[package]]1618[[package]]1272name = "ed25519"1619name = "ed25519"1275checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a"1622checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a"1276dependencies = [1623dependencies = [1277 "pkcs8 0.11.0",1624 "pkcs8 0.11.0",1625 "serdect",1278 "signature 3.0.0",1626 "signature 3.0.0",1279]1627]12801281[[package]]1282name = "ed25519-dalek"1283version = "2.2.0"1284source = "registry+https://github.com/rust-lang/crates.io-index"1285checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"1286dependencies = [1287 "curve25519-dalek 4.1.3",1288 "ed25519 2.2.3",1289 "serde",1290 "sha2 0.10.9",1291 "subtle",1292 "zeroize",1293]129416281295[[package]]1629[[package]]1296name = "ed25519-dalek"1630name = "ed25519-dalek"1299checksum = "b011170fe4f04665565b4110afef66774fe9ffff278f3eb5b81cc73d26e27d60"1633checksum = "b011170fe4f04665565b4110afef66774fe9ffff278f3eb5b81cc73d26e27d60"1300dependencies = [1634dependencies = [1301 "curve25519-dalek 5.0.0-rc.0",1635 "curve25519-dalek 5.0.0-rc.0",1302 "ed25519 3.0.0",1636 "ed25519",1303 "rand_core 0.10.1",1637 "rand_core 0.10.1",1304 "serde",1638 "serde",1305 "sha2 0.11.0",1639 "sha2 0.11.0",1337 "zeroize",1671 "zeroize",1338]1672]16731674[[package]]1675name = "embedded-io"1676version = "0.4.0"1677source = "registry+https://github.com/rust-lang/crates.io-index"1678checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"16791680[[package]]1681name = "embedded-io"1682version = "0.6.1"1683source = "registry+https://github.com/rust-lang/crates.io-index"1684checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"133916851340[[package]]1686[[package]]1341name = "encode_unicode"1687name = "encode_unicode"1349source = "registry+https://github.com/rust-lang/crates.io-index"1695source = "registry+https://github.com/rust-lang/crates.io-index"1350checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"1696checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"16971698[[package]]1699name = "enum-assoc"1700version = "1.3.0"1701source = "registry+https://github.com/rust-lang/crates.io-index"1702checksum = "3ed8956bd5c1f0415200516e78ff07ec9e16415ade83c056c230d7b7ea0d55b7"1703dependencies = [1704 "proc-macro2",1705 "quote",1706 "syn 2.0.118",1707]17081709[[package]]1710name = "enum-repr"1711version = "0.2.6"1712source = "registry+https://github.com/rust-lang/crates.io-index"1713checksum = "bad30c9c0fa1aaf1ae5010dab11f1117b15d35faf62cda4bbbc53b9987950f18"1714dependencies = [1715 "proc-macro2",1716 "quote",1717 "syn 1.0.109",1718]135117191352[[package]]1720[[package]]1353name = "enum_dispatch"1721name = "enum_dispatch"1358 "once_cell",1726 "once_cell",1359 "proc-macro2",1727 "proc-macro2",1360 "quote",1728 "quote",1361 "syn 2.0.117",1729 "syn 2.0.118",1362]1730]136317311364[[package]]1732[[package]]1379dependencies = [1747dependencies = [1380 "proc-macro2",1748 "proc-macro2",1381 "quote",1749 "quote",1382 "syn 2.0.117",1750 "syn 2.0.118",1383]1751]138417521385[[package]]1753[[package]]1505 "futures",1873 "futures",1506 "human-repr",1874 "human-repr",1507 "indicatif",1875 "indicatif",1508 "itertools 0.14.0",1876 "itertools 0.15.0",1509 "nix-eval",1877 "nix-eval",1510 "opentelemetry",1878 "opentelemetry",1511 "opentelemetry-appender-tracing",1879 "opentelemetry-appender-tracing",1514 "remowt-fleet",1882 "remowt-fleet",1515 "serde",1883 "serde",1516 "serde_json",1884 "serde_json",1517 "shlex 1.3.0",1885 "shlex 2.0.1",1518 "tabled",1886 "tabled",1519 "tempfile",1887 "tempfile",1520 "tokio",1888 "tokio",152618941527[[package]]1895[[package]]1528name = "fleet-base"1896name = "fleet-base"1529version = "0.1.0"1897version = "0.1.9"1530dependencies = [1898dependencies = [1531 "age",1899 "age",1532 "anyhow",1900 "anyhow",1537 "futures",1905 "futures",1538 "hostname",1906 "hostname",1539 "indoc",1907 "indoc",1540 "itertools 0.14.0",1908 "itertools 0.15.0",1541 "nix-eval",1909 "nix-eval",1542 "nixlike",1910 "nixlike",1543 "nom 8.0.0",1911 "nom 8.0.0",155819261559[[package]]1927[[package]]1560name = "fleet-generator-helper"1928name = "fleet-generator-helper"1561version = "0.1.0"1929version = "0.1.9"1562dependencies = [1930dependencies = [1563 "age",1931 "age",1564 "anyhow",1932 "anyhow",1565 "base64 0.22.1",1933 "base64 0.22.1",1566 "clap",1934 "clap",1567 "ed25519-dalek 2.2.0",1935 "ed25519-dalek",1568 "fleet-shared",1936 "fleet-shared",1569 "hex",1937 "hex",1570 "hmac 0.12.1",1938 "hmac 0.13.0",1571 "pbkdf2 0.12.2",1939 "pbkdf2 0.12.2",1572 "rand 0.10.1",1940 "rand 0.10.1",1573 "sha2 0.10.9",1941 "sha2 0.10.9",157619441577[[package]]1945[[package]]1578name = "fleet-install-secrets"1946name = "fleet-install-secrets"1579version = "0.1.0"1947version = "0.1.9"1580dependencies = [1948dependencies = [1581 "age",1949 "age",1582 "anyhow",1950 "anyhow",159219601593[[package]]1961[[package]]1594name = "fleet-shared"1962name = "fleet-shared"1595version = "0.1.0"1963version = "0.1.9"1596dependencies = [1964dependencies = [1597 "base64 0.22.1",1965 "base64 0.22.1",1598 "serde",1966 "serde",1649source = "registry+https://github.com/rust-lang/crates.io-index"2017source = "registry+https://github.com/rust-lang/crates.io-index"1650checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"2018checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"16511652[[package]]1653name = "foldhash"1654version = "0.1.5"1655source = "registry+https://github.com/rust-lang/crates.io-index"1656checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"165720191658[[package]]2020[[package]]1659name = "foldhash"2021name = "foldhash"1670 "percent-encoding",2032 "percent-encoding",1671]2033]20342035[[package]]2036name = "fs_extra"2037version = "1.3.0"2038source = "registry+https://github.com/rust-lang/crates.io-index"2039checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"167220401673[[package]]2041[[package]]1674name = "futures"2042name = "futures"1685 "futures-util",2053 "futures-util",1686]2054]20552056[[package]]2057name = "futures-buffered"2058version = "0.2.13"2059source = "registry+https://github.com/rust-lang/crates.io-index"2060checksum = "4421cb78ee172b6b06080093479d3c50f058e7c81b7d577bbb8d118d551d4cd5"2061dependencies = [2062 "cordyceps",2063 "diatomic-waker",2064 "futures-core",2065 "pin-project-lite",2066 "spin 0.10.0",2067]168720681688[[package]]2069[[package]]1689name = "futures-channel"2070name = "futures-channel"1739dependencies = [2120dependencies = [1740 "proc-macro2",2121 "proc-macro2",1741 "quote",2122 "quote",1742 "syn 2.0.117",2123 "syn 2.0.118",1743]2124]174421251745[[package]]2126[[package]]1771 "slab",2152 "slab",1772]2153]21542155[[package]]2156name = "generator"2157version = "0.8.9"2158source = "registry+https://github.com/rust-lang/crates.io-index"2159checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae"2160dependencies = [2161 "cc",2162 "cfg-if",2163 "libc",2164 "log",2165 "rustversion",2166 "windows-link",2167 "windows-result",2168]177321691774[[package]]2170[[package]]1775name = "generic-array"2171name = "generic-array"182122171822[[package]]2218[[package]]1823name = "getrandom"2219name = "getrandom"1824version = "0.4.2"2220version = "0.4.3"1825source = "registry+https://github.com/rust-lang/crates.io-index"2221source = "registry+https://github.com/rust-lang/crates.io-index"1826checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"2222checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"1827dependencies = [2223dependencies = [1828 "cfg-if",2224 "cfg-if",1829 "js-sys",2225 "js-sys",1830 "libc",2226 "libc",1831 "r-efi 6.0.0",2227 "r-efi 6.0.0",1832 "rand_core 0.10.1",2228 "rand_core 0.10.1",1833 "wasip2",1834 "wasip3",1835 "wasm-bindgen",2229 "wasm-bindgen",1836]2230]183722311873 "regex-syntax",2267 "regex-syntax",1874]2268]22692270[[package]]2271name = "gloo-timers"2272version = "0.3.0"2273source = "registry+https://github.com/rust-lang/crates.io-index"2274checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"2275dependencies = [2276 "futures-channel",2277 "futures-core",2278 "js-sys",2279 "wasm-bindgen",2280]187522811876[[package]]2282[[package]]1877name = "group"2283name = "group"188622921887[[package]]2293[[package]]1888name = "h2"2294name = "h2"1889version = "0.4.14"2295version = "0.4.15"1890source = "registry+https://github.com/rust-lang/crates.io-index"2296source = "registry+https://github.com/rust-lang/crates.io-index"1891checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"2297checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"1892dependencies = [2298dependencies = [1893 "atomic-waker",2299 "atomic-waker",1894 "bytes",2300 "bytes",190523111906[[package]]2312[[package]]1907name = "hashbrown"2313name = "hashbrown"1908version = "0.15.5"2314version = "0.17.1"1909source = "registry+https://github.com/rust-lang/crates.io-index"2315source = "registry+https://github.com/rust-lang/crates.io-index"1910checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"2316checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"1911dependencies = [2317dependencies = [2318 "allocator-api2",2319 "equivalent",1912 "foldhash 0.1.5",2320 "foldhash",1913]2321]19141915[[package]]1916name = "hashbrown"1917version = "0.17.1"1918source = "registry+https://github.com/rust-lang/crates.io-index"1919checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"192023221921[[package]]2323[[package]]1922name = "heck"2324name = "heck"1942source = "registry+https://github.com/rust-lang/crates.io-index"2344source = "registry+https://github.com/rust-lang/crates.io-index"1943checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1"2345checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1"23462347[[package]]2348name = "hickory-net"2349version = "0.26.1"2350source = "registry+https://github.com/rust-lang/crates.io-index"2351checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183"2352dependencies = [2353 "async-trait",2354 "bytes",2355 "cfg-if",2356 "data-encoding",2357 "futures-channel",2358 "futures-io",2359 "futures-util",2360 "h2",2361 "hickory-proto",2362 "http",2363 "idna",2364 "ipnet",2365 "jni 0.22.4",2366 "rand 0.10.1",2367 "rustls",2368 "thiserror 2.0.18",2369 "tinyvec",2370 "tokio",2371 "tokio-rustls",2372 "tracing",2373 "url",2374]23752376[[package]]2377name = "hickory-proto"2378version = "0.26.1"2379source = "registry+https://github.com/rust-lang/crates.io-index"2380checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643"2381dependencies = [2382 "data-encoding",2383 "idna",2384 "ipnet",2385 "jni 0.22.4",2386 "once_cell",2387 "prefix-trie",2388 "rand 0.10.1",2389 "ring",2390 "thiserror 2.0.18",2391 "tinyvec",2392 "tracing",2393 "url",2394]23952396[[package]]2397name = "hickory-resolver"2398version = "0.26.1"2399source = "registry+https://github.com/rust-lang/crates.io-index"2400checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c"2401dependencies = [2402 "cfg-if",2403 "futures-util",2404 "hickory-net",2405 "hickory-proto",2406 "ipconfig",2407 "ipnet",2408 "jni 0.22.4",2409 "moka",2410 "ndk-context",2411 "once_cell",2412 "parking_lot",2413 "rand 0.10.1",2414 "resolv-conf",2415 "rustls",2416 "smallvec",2417 "system-configuration",2418 "thiserror 2.0.18",2419 "tokio",2420 "tokio-rustls",2421 "tracing",2422]194424231945[[package]]2424[[package]]1946name = "hkdf"2425name = "hkdf"2037source = "registry+https://github.com/rust-lang/crates.io-index"2516source = "registry+https://github.com/rust-lang/crates.io-index"2038checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"2517checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"25182519[[package]]2520name = "httpdate"2521version = "1.0.3"2522source = "registry+https://github.com/rust-lang/crates.io-index"2523checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"203925242040[[package]]2525[[package]]2041name = "human-repr"2526name = "human-repr"2069 "http",2554 "http",2070 "http-body",2555 "http-body",2071 "httparse",2556 "httparse",2557 "httpdate",2072 "itoa",2558 "itoa",2073 "pin-project-lite",2559 "pin-project-lite",2074 "smallvec",2560 "smallvec",2086 "hyper",2572 "hyper",2087 "hyper-util",2573 "hyper-util",2088 "rustls",2574 "rustls",2089 "rustls-native-certs",2090 "tokio",2575 "tokio",2091 "tokio-rustls",2576 "tokio-rustls",2092 "tower-service",2577 "tower-service",2177 "proc-macro2",2662 "proc-macro2",2178 "quote",2663 "quote",2179 "strsim",2664 "strsim",2180 "syn 2.0.117",2665 "syn 2.0.118",2181 "unic-langid",2666 "unic-langid",2182]2667]218326682191 "i18n-config",2676 "i18n-config",2192 "proc-macro2",2677 "proc-macro2",2193 "quote",2678 "quote",2194 "syn 2.0.117",2679 "syn 2.0.118",2195]2680]219626812197[[package]]2682[[package]]2300 "zerovec",2785 "zerovec",2301]2786]27872788[[package]]2789name = "ident_case"2790version = "1.0.1"2791source = "registry+https://github.com/rust-lang/crates.io-index"2792checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"230227932303[[package]]2794[[package]]2304name = "id-arena"2795name = "identity-hash"2305version = "2.3.0"2796version = "0.1.0"2306source = "registry+https://github.com/rust-lang/crates.io-index"2797source = "registry+https://github.com/rust-lang/crates.io-index"2307checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"2798checksum = "dfdd7caa900436d8f13b2346fe10257e0c05c1f1f9e351f4f5d57c03bd5f45da"230827992309[[package]]2800[[package]]2310name = "idna"2801name = "idna"2327 "icu_properties",2818 "icu_properties",2328]2819]28202821[[package]]2822name = "igd-next"2823version = "0.17.1"2824source = "registry+https://github.com/rust-lang/crates.io-index"2825checksum = "de7238d487a9aff61f81b5ab41c0a841532a115a398b5fa92a2fadd0885e2581"2826dependencies = [2827 "attohttpc",2828 "bytes",2829 "futures",2830 "http",2831 "http-body-util",2832 "hyper",2833 "hyper-util",2834 "log",2835 "rand 0.10.1",2836 "tokio",2837 "url",2838 "xmltree",2839]232928402330[[package]]2841[[package]]2331name = "indexmap"2842name = "indexmap"2334checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"2845checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"2335dependencies = [2846dependencies = [2336 "equivalent",2847 "equivalent",2337 "hashbrown 0.17.1",2848 "hashbrown",2338 "serde",2339 "serde_core",2340]2849]234128502342[[package]]2851[[package]]2419source = "registry+https://github.com/rust-lang/crates.io-index"2928source = "registry+https://github.com/rust-lang/crates.io-index"2420checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304"2929checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304"29302931[[package]]2932name = "ipconfig"2933version = "0.3.4"2934source = "registry+https://github.com/rust-lang/crates.io-index"2935checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222"2936dependencies = [2937 "socket2",2938 "widestring",2939 "windows-registry",2940 "windows-result",2941 "windows-sys 0.61.2",2942]242129432422[[package]]2944[[package]]2423name = "ipnet"2945name = "ipnet"2424version = "2.12.0"2946version = "2.12.0"2425source = "registry+https://github.com/rust-lang/crates.io-index"2947source = "registry+https://github.com/rust-lang/crates.io-index"2426checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"2948checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"2949dependencies = [2950 "serde",2951]29522953[[package]]2954name = "iroh"2955version = "1.0.0"2956source = "registry+https://github.com/rust-lang/crates.io-index"2957checksum = "6435544bb3a5c4e6ff7affaa0c0aa0d1bca45bd700226329d5059d3eb54f9dff"2958dependencies = [2959 "backon",2960 "blake3",2961 "bytes",2962 "cfg_aliases",2963 "ctutils",2964 "data-encoding",2965 "derive_more",2966 "ed25519-dalek",2967 "futures-util",2968 "getrandom 0.4.3",2969 "hickory-resolver",2970 "http",2971 "ipnet",2972 "iroh-base",2973 "iroh-dns",2974 "iroh-metrics",2975 "iroh-relay",2976 "n0-error",2977 "n0-future",2978 "n0-watcher",2979 "netwatch",2980 "noq",2981 "noq-proto",2982 "noq-udp",2983 "papaya",2984 "pin-project",2985 "portable-atomic",2986 "portmapper",2987 "rand 0.10.1",2988 "reqwest",2989 "rustc-hash 2.1.2",2990 "rustls",2991 "rustls-pki-types",2992 "serde",2993 "smallvec",2994 "strum",2995 "time",2996 "tokio",2997 "tokio-stream",2998 "tokio-util",2999 "tracing",3000 "url",3001 "wasm-bindgen-futures",3002]30033004[[package]]3005name = "iroh-base"3006version = "1.0.0"3007source = "registry+https://github.com/rust-lang/crates.io-index"3008checksum = "c9c95e4459d9bb828a77084277abd308aa2b58a096652b079bddfd6ef2361f53"3009dependencies = [3010 "curve25519-dalek 5.0.0-rc.0",3011 "data-encoding",3012 "data-encoding-macro",3013 "derive_more",3014 "ed25519-dalek",3015 "getrandom 0.4.3",3016 "n0-error",3017 "rand 0.10.1",3018 "serde",3019 "url",3020 "zeroize",3021]30223023[[package]]3024name = "iroh-dns"3025version = "1.0.0"3026source = "registry+https://github.com/rust-lang/crates.io-index"3027checksum = "754f7e0c1f67938e1d671007264ffef158f14a9f795a7cc219ea68ea09a9d4c9"3028dependencies = [3029 "arc-swap",3030 "cfg_aliases",3031 "derive_more",3032 "hickory-resolver",3033 "iroh-base",3034 "n0-error",3035 "n0-future",3036 "ndk-context",3037 "portable-atomic",3038 "rand 0.10.1",3039 "rustls",3040 "simple-dns",3041 "strum",3042 "tokio",3043 "tracing",3044 "url",3045]30463047[[package]]3048name = "iroh-metrics"3049version = "1.0.1"3050source = "registry+https://github.com/rust-lang/crates.io-index"3051checksum = "291065721ad7c477b972e581bbc528df031dc8eb5e39fe1ff3300ae5dfb157ef"3052dependencies = [3053 "iroh-metrics-derive",3054 "itoa",3055 "n0-error",3056 "portable-atomic",3057 "ryu",3058 "serde",3059 "tracing",3060]30613062[[package]]3063name = "iroh-metrics-derive"3064version = "1.0.1"3065source = "registry+https://github.com/rust-lang/crates.io-index"3066checksum = "1ae5f0c4405d1fbc9fb16ff422ca40620e93dc36c30ecaba0c2aee3992b7bd48"3067dependencies = [3068 "heck",3069 "proc-macro2",3070 "quote",3071 "syn 2.0.118",3072]30733074[[package]]3075name = "iroh-relay"3076version = "1.0.0"3077source = "registry+https://github.com/rust-lang/crates.io-index"3078checksum = "1c12e48fef252fd04f8e6b6a8802b377baf72548d62ae4838816624cd0e06b79"3079dependencies = [3080 "blake3",3081 "bytes",3082 "cfg_aliases",3083 "data-encoding",3084 "derive_more",3085 "getrandom 0.4.3",3086 "hickory-resolver",3087 "http",3088 "http-body-util",3089 "hyper",3090 "hyper-util",3091 "iroh-base",3092 "iroh-dns",3093 "iroh-metrics",3094 "lru",3095 "n0-error",3096 "n0-future",3097 "noq",3098 "noq-proto",3099 "num_enum",3100 "pin-project",3101 "postcard",3102 "rand 0.10.1",3103 "reqwest",3104 "rustls",3105 "rustls-pki-types",3106 "serde",3107 "serde_bytes",3108 "strum",3109 "tokio",3110 "tokio-rustls",3111 "tokio-util",3112 "tokio-websockets",3113 "tracing",3114 "url",3115 "vergen-gitcl",3116 "webpki-roots",3117 "ws_stream_wasm",3118]242731192428[[package]]3120[[package]]2429name = "is_terminal_polyfill"3121name = "is_terminal_polyfill"2430version = "1.70.2"3122version = "1.70.2"2431source = "registry+https://github.com/rust-lang/crates.io-index"3123source = "registry+https://github.com/rust-lang/crates.io-index"2432checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"3124checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"31253126[[package]]3127name = "itertools"3128version = "0.12.1"3129source = "registry+https://github.com/rust-lang/crates.io-index"3130checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"3131dependencies = [3132 "either",3133]243331342434[[package]]3135[[package]]2435name = "itertools"3136name = "itertools"2449 "either",3150 "either",2450]3151]31523153[[package]]3154name = "itertools"3155version = "0.15.0"3156source = "registry+https://github.com/rust-lang/crates.io-index"3157checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc"3158dependencies = [3159 "either",3160]245131612452[[package]]3162[[package]]2453name = "itoa"3163name = "itoa"2454version = "1.0.18"3164version = "1.0.18"2455source = "registry+https://github.com/rust-lang/crates.io-index"3165source = "registry+https://github.com/rust-lang/crates.io-index"2456checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"3166checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"31673168[[package]]3169name = "jni"3170version = "0.21.1"3171source = "registry+https://github.com/rust-lang/crates.io-index"3172checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"3173dependencies = [3174 "cesu8",3175 "cfg-if",3176 "combine",3177 "jni-sys 0.3.1",3178 "log",3179 "thiserror 1.0.69",3180 "walkdir",3181 "windows-sys 0.45.0",3182]31833184[[package]]3185name = "jni"3186version = "0.22.4"3187source = "registry+https://github.com/rust-lang/crates.io-index"3188checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"3189dependencies = [3190 "cfg-if",3191 "combine",3192 "jni-macros",3193 "jni-sys 0.4.1",3194 "log",3195 "simd_cesu8",3196 "thiserror 2.0.18",3197 "walkdir",3198 "windows-link",3199]32003201[[package]]3202name = "jni-macros"3203version = "0.22.4"3204source = "registry+https://github.com/rust-lang/crates.io-index"3205checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"3206dependencies = [3207 "proc-macro2",3208 "quote",3209 "rustc_version",3210 "simd_cesu8",3211 "syn 2.0.118",3212]32133214[[package]]3215name = "jni-sys"3216version = "0.3.1"3217source = "registry+https://github.com/rust-lang/crates.io-index"3218checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"3219dependencies = [3220 "jni-sys 0.4.1",3221]32223223[[package]]3224name = "jni-sys"3225version = "0.4.1"3226source = "registry+https://github.com/rust-lang/crates.io-index"3227checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"3228dependencies = [3229 "jni-sys-macros",3230]32313232[[package]]3233name = "jni-sys-macros"3234version = "0.4.1"3235source = "registry+https://github.com/rust-lang/crates.io-index"3236checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"3237dependencies = [3238 "quote",3239 "syn 2.0.118",3240]32413242[[package]]3243name = "jobserver"3244version = "0.1.34"3245source = "registry+https://github.com/rust-lang/crates.io-index"3246checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"3247dependencies = [3248 "getrandom 0.3.4",3249 "libc",3250]245732512458[[package]]3252[[package]]2459name = "js-sys"3253name = "js-sys"2492source = "registry+https://github.com/rust-lang/crates.io-index"3286source = "registry+https://github.com/rust-lang/crates.io-index"2493checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"3287checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"2494dependencies = [3288dependencies = [2495 "spin",3289 "spin 0.9.8",2496]3290]249732912498[[package]]3292[[package]]2499name = "leb128fmt"3293name = "lazycell"2500version = "0.1.0"3294version = "1.3.0"2501source = "registry+https://github.com/rust-lang/crates.io-index"3295source = "registry+https://github.com/rust-lang/crates.io-index"2502checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"3296checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"250332972504[[package]]3298[[package]]2505name = "libc"3299name = "libc"2529source = "registry+https://github.com/rust-lang/crates.io-index"3323source = "registry+https://github.com/rust-lang/crates.io-index"2530checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3"3324checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3"2531dependencies = [3325dependencies = [2532 "bitflags",3326 "bitflags 2.13.0",2533 "libc",3327 "libc",2534 "plain",3328 "plain",2535 "redox_syscall 0.8.1",3329 "redox_syscall 0.8.1",2583source = "registry+https://github.com/rust-lang/crates.io-index"3377source = "registry+https://github.com/rust-lang/crates.io-index"2584checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"3378checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"33793380[[package]]3381name = "loom"3382version = "0.7.2"3383source = "registry+https://github.com/rust-lang/crates.io-index"3384checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"3385dependencies = [3386 "cfg-if",3387 "generator",3388 "scoped-tls",3389 "tracing",3390 "tracing-subscriber",3391]33923393[[package]]3394name = "lru"3395version = "0.18.0"3396source = "registry+https://github.com/rust-lang/crates.io-index"3397checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9"3398dependencies = [3399 "hashbrown",3400]258534012586[[package]]3402[[package]]2587name = "lru-slab"3403name = "lru-slab"2588version = "0.1.2"3404version = "0.1.2"2589source = "registry+https://github.com/rust-lang/crates.io-index"3405source = "registry+https://github.com/rust-lang/crates.io-index"2590checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"3406checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"34073408[[package]]3409name = "mac-addr"3410version = "0.3.0"3411source = "registry+https://github.com/rust-lang/crates.io-index"3412checksum = "d3d25b0e0b648a86960ac23b7ad4abb9717601dec6f66c165f5b037f3f03065f"259134132592[[package]]3414[[package]]2593name = "matchers"3415name = "matchers"2671 "num-traits",3493 "num-traits",2672]3494]34953496[[package]]3497name = "moka"3498version = "0.12.15"3499source = "registry+https://github.com/rust-lang/crates.io-index"3500checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046"3501dependencies = [3502 "crossbeam-channel",3503 "crossbeam-epoch",3504 "crossbeam-utils",3505 "equivalent",3506 "parking_lot",3507 "portable-atomic",3508 "smallvec",3509 "tagptr",3510 "uuid",3511]35123513[[package]]3514name = "n0-error"3515version = "1.0.0"3516source = "registry+https://github.com/rust-lang/crates.io-index"3517checksum = "c37e81176a83a77d2514528b91bdafc70ef88aab428f0e1b91aebb8d99888895"3518dependencies = [3519 "n0-error-macros",3520 "spez",3521]35223523[[package]]3524name = "n0-error-macros"3525version = "1.0.0"3526source = "registry+https://github.com/rust-lang/crates.io-index"3527checksum = "e2acd8b070213b0299282f884b4beba4e7b52d624fdcd504a3ad3665390c11e1"3528dependencies = [3529 "proc-macro2",3530 "quote",3531 "syn 2.0.118",3532]35333534[[package]]3535name = "n0-future"3536version = "0.3.2"3537source = "registry+https://github.com/rust-lang/crates.io-index"3538checksum = "e2ab99dfb861450e68853d34ae665243a88b8c493d01ba957321a1e9b2312bbe"3539dependencies = [3540 "cfg_aliases",3541 "derive_more",3542 "futures-buffered",3543 "futures-lite",3544 "futures-util",3545 "js-sys",3546 "pin-project",3547 "send_wrapper",3548 "tokio",3549 "tokio-util",3550 "wasm-bindgen",3551 "wasm-bindgen-futures",3552 "web-time",3553]35543555[[package]]3556name = "n0-watcher"3557version = "1.0.0"3558source = "registry+https://github.com/rust-lang/crates.io-index"3559checksum = "bbc618745ad0b7414b149d0517ad8b5573b2fb4d4e2717add3d2446ce1fdd826"3560dependencies = [3561 "derive_more",3562 "n0-error",3563 "n0-future",3564]35653566[[package]]3567name = "ndk-context"3568version = "0.1.1"3569source = "registry+https://github.com/rust-lang/crates.io-index"3570checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"35713572[[package]]3573name = "netdev"3574version = "0.44.0"3575source = "registry+https://github.com/rust-lang/crates.io-index"3576checksum = "9d31e7286c21ceaf0ddb1d881964011214555ea0b317cc2eb1a1d68d861386fc"3577dependencies = [3578 "block2",3579 "dispatch2",3580 "dlopen2",3581 "ipnet",3582 "jni 0.21.1",3583 "libc",3584 "mac-addr",3585 "ndk-context",3586 "netlink-packet-core",3587 "netlink-packet-route 0.29.0",3588 "netlink-sys",3589 "objc2",3590 "objc2-core-foundation",3591 "objc2-core-wlan",3592 "objc2-foundation",3593 "objc2-system-configuration",3594 "once_cell",3595 "plist",3596 "windows-sys 0.61.2",3597]35983599[[package]]3600name = "netlink-packet-core"3601version = "0.8.1"3602source = "registry+https://github.com/rust-lang/crates.io-index"3603checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4"3604dependencies = [3605 "paste",3606]36073608[[package]]3609name = "netlink-packet-route"3610version = "0.29.0"3611source = "registry+https://github.com/rust-lang/crates.io-index"3612checksum = "df9854ea6ad14e3f4698a7f03b65bce0833dd2d81d594a0e4a984170537146b6"3613dependencies = [3614 "bitflags 2.13.0",3615 "libc",3616 "log",3617 "netlink-packet-core",3618]36193620[[package]]3621name = "netlink-packet-route"3622version = "0.31.0"3623source = "registry+https://github.com/rust-lang/crates.io-index"3624checksum = "e2288fcb784eb3defd5fb16f4c4160d5f477de192eac730f43e1d11c24d9a007"3625dependencies = [3626 "bitflags 2.13.0",3627 "libc",3628 "log",3629 "netlink-packet-core",3630]36313632[[package]]3633name = "netlink-proto"3634version = "0.12.0"3635source = "registry+https://github.com/rust-lang/crates.io-index"3636checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128"3637dependencies = [3638 "bytes",3639 "futures",3640 "log",3641 "netlink-packet-core",3642 "netlink-sys",3643 "thiserror 2.0.18",3644]36453646[[package]]3647name = "netlink-sys"3648version = "0.8.8"3649source = "registry+https://github.com/rust-lang/crates.io-index"3650checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae"3651dependencies = [3652 "bytes",3653 "futures-util",3654 "libc",3655 "log",3656 "tokio",3657]36583659[[package]]3660name = "netwatch"3661version = "0.19.0"3662source = "registry+https://github.com/rust-lang/crates.io-index"3663checksum = "8487d26d691cd98d5c17b2adb4b1fd4b31cccc820da1eac827d483295d7bb94a"3664dependencies = [3665 "atomic-waker",3666 "bytes",3667 "cfg_aliases",3668 "derive_more",3669 "ipnet",3670 "js-sys",3671 "libc",3672 "n0-error",3673 "n0-future",3674 "n0-watcher",3675 "netdev",3676 "netlink-packet-core",3677 "netlink-packet-route 0.31.0",3678 "netlink-proto",3679 "netlink-sys",3680 "noq-udp",3681 "objc2-core-foundation",3682 "objc2-system-configuration",3683 "pin-project-lite",3684 "serde",3685 "socket2",3686 "time",3687 "tokio",3688 "tokio-util",3689 "tracing",3690 "web-sys",3691 "windows",3692 "windows-result",3693 "wmi",3694]267336952674[[package]]3696[[package]]2675name = "nix"3697name = "nix"2676version = "0.31.3"3698version = "0.31.3"2677source = "registry+https://github.com/rust-lang/crates.io-index"3699source = "registry+https://github.com/rust-lang/crates.io-index"2678checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"3700checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"2679dependencies = [3701dependencies = [2680 "bitflags",3702 "bitflags 2.13.0",2681 "cfg-if",3703 "cfg-if",2682 "cfg_aliases",3704 "cfg_aliases",2683 "libc",3705 "libc",2684]3706]268537072686[[package]]3708[[package]]2687name = "nix-eval"3709name = "nix-eval"2688version = "0.1.0"3710version = "0.1.9"2689dependencies = [3711dependencies = [2690 "anyhow",3712 "anyhow",2691 "bindgen",3713 "bindgen 0.72.1",2692 "camino",3714 "camino",2693 "cxx",3715 "cxx",2694 "cxx-build",3716 "cxx-build",2695 "futures",3717 "futures",2696 "itertools 0.14.0",3718 "itertools 0.15.0",2697 "nixlike",3719 "nixlike",2698 "pkg-config",3720 "pkg-config",2699 "serde",3721 "serde",270737292708[[package]]3730[[package]]2709name = "nix-plugin-fleet"3731name = "nix-plugin-fleet"2710version = "0.1.0"3732version = "0.1.9"2711dependencies = [3733dependencies = [2712 "fleet-base",3734 "fleet-base",2713]3735]271437362715[[package]]3737[[package]]2716name = "nixlike"3738name = "nixlike"2717version = "0.1.0"3739version = "0.1.9"2718dependencies = [3740dependencies = [2719 "itertools 0.14.0",3741 "itertools 0.15.0",2720 "linked-hash-map",3742 "linked-hash-map",2721 "peg",3743 "peg",2722 "serde",3744 "serde",2742 "memchr",3764 "memchr",2743]3765]37663767[[package]]3768name = "noq"3769version = "1.0.0"3770source = "registry+https://github.com/rust-lang/crates.io-index"3771checksum = "11f0c73794bfde94db01379c46990b9a773993fca2b61a66184ce148b7c7a187"3772dependencies = [3773 "bytes",3774 "cfg_aliases",3775 "derive_more",3776 "noq-proto",3777 "noq-udp",3778 "pin-project-lite",3779 "rustc-hash 2.1.2",3780 "rustls",3781 "socket2",3782 "thiserror 2.0.18",3783 "tokio",3784 "tokio-stream",3785 "tracing",3786 "web-time",3787]37883789[[package]]3790name = "noq-proto"3791version = "1.0.0"3792source = "registry+https://github.com/rust-lang/crates.io-index"3793checksum = "775be06b8d66c2c64db60140bf54dee8410f67b73c81cc1e1e32f11dfdaae501"3794dependencies = [3795 "aes-gcm 0.10.3",3796 "bytes",3797 "derive_more",3798 "enum-assoc",3799 "getrandom 0.4.3",3800 "identity-hash",3801 "lru-slab",3802 "rand 0.10.1",3803 "rand_pcg",3804 "ring",3805 "rustc-hash 2.1.2",3806 "rustls",3807 "rustls-pki-types",3808 "slab",3809 "sorted-index-buffer",3810 "thiserror 2.0.18",3811 "tinyvec",3812 "tracing",3813 "web-time",3814]38153816[[package]]3817name = "noq-udp"3818version = "1.0.0"3819source = "registry+https://github.com/rust-lang/crates.io-index"3820checksum = "cd5a37756f168cf350d68a97c4f0158bdf3c76f10175123941569b09ab51f011"3821dependencies = [3822 "cfg_aliases",3823 "libc",3824 "socket2",3825 "tracing",3826 "windows-sys 0.61.2",3827]274438282745[[package]]3829[[package]]2746name = "nu-ansi-term"3830name = "nu-ansi-term"2813 "libm",3897 "libm",2814]3898]38993900[[package]]3901name = "num_enum"3902version = "0.7.6"3903source = "registry+https://github.com/rust-lang/crates.io-index"3904checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"3905dependencies = [3906 "num_enum_derive",3907 "rustversion",3908]39093910[[package]]3911name = "num_enum_derive"3912version = "0.7.6"3913source = "registry+https://github.com/rust-lang/crates.io-index"3914checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"3915dependencies = [3916 "proc-macro-crate",3917 "proc-macro2",3918 "quote",3919 "syn 2.0.118",3920]39213922[[package]]3923name = "num_threads"3924version = "0.1.7"3925source = "registry+https://github.com/rust-lang/crates.io-index"3926checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"3927dependencies = [3928 "libc",3929]39303931[[package]]3932name = "objc2"3933version = "0.6.4"3934source = "registry+https://github.com/rust-lang/crates.io-index"3935checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"3936dependencies = [3937 "objc2-encode",3938]39393940[[package]]3941name = "objc2-core-foundation"3942version = "0.3.2"3943source = "registry+https://github.com/rust-lang/crates.io-index"3944checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"3945dependencies = [3946 "bitflags 2.13.0",3947 "block2",3948 "dispatch2",3949 "libc",3950 "objc2",3951]39523953[[package]]3954name = "objc2-core-wlan"3955version = "0.3.2"3956source = "registry+https://github.com/rust-lang/crates.io-index"3957checksum = "c71e34919aba0d701380d911702455038a8a3587467fe0141d6a71501e7ffe48"3958dependencies = [3959 "bitflags 2.13.0",3960 "objc2",3961 "objc2-core-foundation",3962 "objc2-foundation",3963 "objc2-security",3964 "objc2-security-foundation",3965]39663967[[package]]3968name = "objc2-encode"3969version = "4.1.0"3970source = "registry+https://github.com/rust-lang/crates.io-index"3971checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"39723973[[package]]3974name = "objc2-foundation"3975version = "0.3.2"3976source = "registry+https://github.com/rust-lang/crates.io-index"3977checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"3978dependencies = [3979 "bitflags 2.13.0",3980 "block2",3981 "libc",3982 "objc2",3983 "objc2-core-foundation",3984]39853986[[package]]3987name = "objc2-security"3988version = "0.3.2"3989source = "registry+https://github.com/rust-lang/crates.io-index"3990checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a"3991dependencies = [3992 "bitflags 2.13.0",3993 "objc2",3994 "objc2-core-foundation",3995]39963997[[package]]3998name = "objc2-security-foundation"3999version = "0.3.2"4000source = "registry+https://github.com/rust-lang/crates.io-index"4001checksum = "ef76382e9cedd18123099f17638715cc3d81dba3637d4c0d39ab69df2ef345a5"4002dependencies = [4003 "objc2",4004 "objc2-foundation",4005]40064007[[package]]4008name = "objc2-system-configuration"4009version = "0.3.2"4010source = "registry+https://github.com/rust-lang/crates.io-index"4011checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396"4012dependencies = [4013 "bitflags 2.13.0",4014 "dispatch2",4015 "libc",4016 "objc2",4017 "objc2-core-foundation",4018 "objc2-security",4019]281540202816[[package]]4021[[package]]2817name = "once_cell"4022name = "once_cell"2818version = "1.21.4"4023version = "1.21.4"2819source = "registry+https://github.com/rust-lang/crates.io-index"4024source = "registry+https://github.com/rust-lang/crates.io-index"2820checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"4025checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"4026dependencies = [4027 "critical-section",4028 "portable-atomic",4029]282140302822[[package]]4031[[package]]2823name = "once_cell_polyfill"4032name = "once_cell_polyfill"283940482840[[package]]4049[[package]]2841name = "opentelemetry"4050name = "opentelemetry"2842version = "0.31.0"4051version = "0.32.0"2843source = "registry+https://github.com/rust-lang/crates.io-index"4052source = "registry+https://github.com/rust-lang/crates.io-index"2844checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0"4053checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682"2845dependencies = [4054dependencies = [2846 "futures-core",4055 "futures-core",2847 "futures-sink",4056 "futures-sink",285340622854[[package]]4063[[package]]2855name = "opentelemetry-appender-tracing"4064name = "opentelemetry-appender-tracing"2856version = "0.31.1"4065version = "0.32.0"2857source = "registry+https://github.com/rust-lang/crates.io-index"4066source = "registry+https://github.com/rust-lang/crates.io-index"2858checksum = "ef6a1ac5ca3accf562b8c306fa8483c85f4390f768185ab775f242f7fe8fdcc2"4067checksum = "2c0080f0dc1d7c786f467cd85a4e395fcab11ee852004f39a29a18ab7c25d837"2859dependencies = [4068dependencies = [2860 "opentelemetry",4069 "opentelemetry",2861 "tracing",4070 "tracing",286540742866[[package]]4075[[package]]2867name = "opentelemetry-exporter-env"4076name = "opentelemetry-exporter-env"2868version = "0.1.0"4077version = "0.1.9"2869dependencies = [4078dependencies = [2870 "clap",4079 "clap",2871 "opentelemetry-otlp",4080 "opentelemetry-otlp",287440832875[[package]]4084[[package]]2876name = "opentelemetry-http"4085name = "opentelemetry-http"2877version = "0.31.0"4086version = "0.32.0"2878source = "registry+https://github.com/rust-lang/crates.io-index"4087source = "registry+https://github.com/rust-lang/crates.io-index"2879checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d"4088checksum = "5683015d09e2df236ef005b17f6f196f0d5f6313c4fa43a7b6a53b52776e4331"2880dependencies = [4089dependencies = [2881 "async-trait",4090 "async-trait",2882 "bytes",4091 "bytes",288740962888[[package]]4097[[package]]2889name = "opentelemetry-otlp"4098name = "opentelemetry-otlp"2890version = "0.31.1"4099version = "0.32.0"2891source = "registry+https://github.com/rust-lang/crates.io-index"4100source = "registry+https://github.com/rust-lang/crates.io-index"2892checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f"4101checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35"2893dependencies = [4102dependencies = [2894 "http",4103 "http",2895 "opentelemetry",4104 "opentelemetry",2902 "thiserror 2.0.18",4111 "thiserror 2.0.18",2903 "tokio",4112 "tokio",2904 "tonic",4113 "tonic",2905 "tracing",4114 "tonic-types",2906]4115]290741162908[[package]]4117[[package]]2909name = "opentelemetry-proto"4118name = "opentelemetry-proto"2910version = "0.31.0"4119version = "0.32.0"2911source = "registry+https://github.com/rust-lang/crates.io-index"4120source = "registry+https://github.com/rust-lang/crates.io-index"2912checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f"4121checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638"2913dependencies = [4122dependencies = [2914 "base64 0.22.1",4123 "base64 0.22.1",2915 "const-hex",4124 "const-hex",2916 "opentelemetry",4125 "opentelemetry",2917 "opentelemetry_sdk",4126 "opentelemetry_sdk",2918 "prost",4127 "prost",2919 "serde",4128 "serde",2920 "serde_json",2921 "tonic",4129 "tonic",2922 "tonic-prost",4130 "tonic-prost",2923]4131]292441322925[[package]]4133[[package]]2926name = "opentelemetry_sdk"4134name = "opentelemetry_sdk"2927version = "0.31.0"4135version = "0.32.1"2928source = "registry+https://github.com/rust-lang/crates.io-index"4136source = "registry+https://github.com/rust-lang/crates.io-index"2929checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd"4137checksum = "9b59f80e1ac4d5ff7a2db8fb6c80badb7f0f3f858211fba08dd9aaec750894f9"2930dependencies = [4138dependencies = [2931 "futures-channel",4139 "futures-channel",2932 "futures-executor",4140 "futures-executor",2933 "futures-util",4141 "futures-util",2934 "opentelemetry",4142 "opentelemetry",2935 "percent-encoding",4143 "percent-encoding",4144 "portable-atomic",2936 "rand 0.9.4",4145 "rand 0.9.4",2937 "thiserror 2.0.18",4146 "thiserror 2.0.18",2938]4147]3008 "windows-strings",4217 "windows-strings",3009]4218]42194220[[package]]4221name = "pam-client"4222version = "0.5.0"4223source = "registry+https://github.com/rust-lang/crates.io-index"4224checksum = "51bd776116a7ada5ebbe31f54cdc5b1030ed7265686cf7c8a21c057a2f8dab9a"4225dependencies = [4226 "bitflags 1.3.2",4227 "enum-repr",4228 "libc",4229 "pam-sys",4230 "rpassword",4231 "rustversion",4232]42334234[[package]]4235name = "pam-sys"4236version = "1.0.0-alpha5"4237source = "registry+https://github.com/rust-lang/crates.io-index"4238checksum = "ce9484729b3e52c0bacdc5191cb6a6a5f31ef4c09c5e4ab1209d3340ad9e997b"4239dependencies = [4240 "bindgen 0.69.5",4241 "libc",4242]42434244[[package]]4245name = "papaya"4246version = "0.2.4"4247source = "registry+https://github.com/rust-lang/crates.io-index"4248checksum = "997ee03cd38c01469a7046643714f0ad28880bcb9e6679ff0666e24817ca19b7"4249dependencies = [4250 "equivalent",4251 "seize",4252]301042533011[[package]]4254[[package]]3012name = "papergrid"4255name = "papergrid"3013version = "0.17.0"4256version = "0.18.0"3014source = "registry+https://github.com/rust-lang/crates.io-index"4257source = "registry+https://github.com/rust-lang/crates.io-index"3015checksum = "6978128c8b51d8f4080631ceb2302ab51e32cc6e8615f735ee2f83fd269ae3f1"4258checksum = "d0984e668274d34691bc2b262ef0d115de5fa9973bcdee7ae32213f93099153e"3016dependencies = [4259dependencies = [3017 "bytecount",4260 "bytecount",3018 "fnv",4261 "fnv",3125source = "registry+https://github.com/rust-lang/crates.io-index"4368source = "registry+https://github.com/rust-lang/crates.io-index"3126checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"4369checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"43704371[[package]]4372name = "pharos"4373version = "0.5.3"4374source = "registry+https://github.com/rust-lang/crates.io-index"4375checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414"4376dependencies = [4377 "futures",4378 "rustc_version",4379]312743803128[[package]]4381[[package]]3129name = "phc"4382name = "phc"3152dependencies = [4405dependencies = [3153 "proc-macro2",4406 "proc-macro2",3154 "quote",4407 "quote",3155 "syn 2.0.117",4408 "syn 2.0.118",3156]4409]315744103158[[package]]4411[[package]]3243source = "registry+https://github.com/rust-lang/crates.io-index"4496source = "registry+https://github.com/rust-lang/crates.io-index"3244checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"4497checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"44984499[[package]]4500name = "plist"4501version = "1.9.0"4502source = "registry+https://github.com/rust-lang/crates.io-index"4503checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1"4504dependencies = [4505 "base64 0.22.1",4506 "indexmap",4507 "quick-xml",4508 "serde",4509 "time",4510]45114512[[package]]4513name = "polkit-backend"4514version = "0.1.9"4515dependencies = [4516 "anyhow",4517 "clap",4518 "nix",4519 "pam-client",4520 "remowt-polkit-shared",4521 "remowt-ui-prompt",4522 "tokio",4523 "tracing",4524 "tracing-subscriber",4525 "zbus",4526 "zbus_polkit",4527]324545283246[[package]]4529[[package]]3247name = "polling"4530name = "polling"3307version = "1.13.1"4590version = "1.13.1"3308source = "registry+https://github.com/rust-lang/crates.io-index"4591source = "registry+https://github.com/rust-lang/crates.io-index"3309checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"4592checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"4593dependencies = [4594 "serde",4595]45964597[[package]]4598name = "portmapper"4599version = "0.19.0"4600source = "registry+https://github.com/rust-lang/crates.io-index"4601checksum = "ccc716c56a0a50f7e4e25f41446419599d47c6197cc5c9858174220e97c272e6"4602dependencies = [4603 "base64 0.22.1",4604 "bytes",4605 "derive_more",4606 "hyper-util",4607 "igd-next",4608 "iroh-metrics",4609 "libc",4610 "n0-error",4611 "n0-future",4612 "netwatch",4613 "num_enum",4614 "rand 0.10.1",4615 "serde",4616 "smallvec",4617 "socket2",4618 "time",4619 "tokio",4620 "tokio-util",4621 "tower-layer",4622 "tracing",4623 "url",4624]46254626[[package]]4627name = "postcard"4628version = "1.1.3"4629source = "registry+https://github.com/rust-lang/crates.io-index"4630checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"4631dependencies = [4632 "cobs",4633 "embedded-io 0.4.0",4634 "embedded-io 0.6.1",4635 "postcard-derive",4636 "serde",4637]46384639[[package]]4640name = "postcard-derive"4641version = "0.2.2"4642source = "registry+https://github.com/rust-lang/crates.io-index"4643checksum = "e0232bd009a197ceec9cc881ba46f727fcd8060a2d8d6a9dde7a69030a6fe2bb"4644dependencies = [4645 "proc-macro2",4646 "quote",4647 "syn 2.0.118",4648]331046493311[[package]]4650[[package]]3312name = "potential_utf"4651name = "potential_utf"3332 "zerocopy",4671 "zerocopy",3333]4672]46734674[[package]]4675name = "prefix-trie"4676version = "0.8.4"4677source = "registry+https://github.com/rust-lang/crates.io-index"4678checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7"4679dependencies = [4680 "either",4681 "ipnet",4682 "num-traits",4683]333446843335[[package]]4685[[package]]3336name = "prettyplease"4686name = "prettyplease"3339checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"4689checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"3340dependencies = [4690dependencies = [3341 "proc-macro2",4691 "proc-macro2",3342 "syn 2.0.117",4692 "syn 2.0.118",3343]4693]334446943345[[package]]4695[[package]]3346name = "primefield"4696name = "primefield"3347version = "0.14.0-rc.11"4697version = "0.14.0-rc.12"3348source = "registry+https://github.com/rust-lang/crates.io-index"4698source = "registry+https://github.com/rust-lang/crates.io-index"3349checksum = "b1d7e42f46a29abc16fb621a3466ee453358ebaae48a9e515f287e0af052ed8f"4699checksum = "8675564771a62f69a0af716b03e89b917b963c7b173b5855575e84fd4f605ca0"3350dependencies = [4700dependencies = [3351 "crypto-bigint",4701 "crypto-bigint",3352 "crypto-common 0.2.2",4702 "crypto-common 0.2.2",3393 "proc-macro-error-attr2",4743 "proc-macro-error-attr2",3394 "proc-macro2",4744 "proc-macro2",3395 "quote",4745 "quote",3396 "syn 2.0.117",4746 "syn 2.0.118",3397]4747]339847483399[[package]]4749[[package]]3411source = "registry+https://github.com/rust-lang/crates.io-index"4761source = "registry+https://github.com/rust-lang/crates.io-index"3412checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"4762checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"3413dependencies = [4763dependencies = [3414 "bitflags",4764 "bitflags 2.13.0",3415 "num-traits",4765 "num-traits",3416 "rand 0.9.4",4766 "rand 0.9.4",3417 "rand_chacha 0.9.0",4767 "rand_chacha 0.9.0",3440 "itertools 0.14.0",4790 "itertools 0.14.0",3441 "proc-macro2",4791 "proc-macro2",3442 "quote",4792 "quote",3443 "syn 2.0.117",4793 "syn 2.0.118",3444]4794]47954796[[package]]4797name = "prost-types"4798version = "0.14.4"4799source = "registry+https://github.com/rust-lang/crates.io-index"4800checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a"4801dependencies = [4802 "prost",4803]48044805[[package]]4806name = "quick-xml"4807version = "0.39.4"4808source = "registry+https://github.com/rust-lang/crates.io-index"4809checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e"4810dependencies = [4811 "memchr",4812]344548133446[[package]]4814[[package]]3447name = "quinn"4815name = "quinn"3469source = "registry+https://github.com/rust-lang/crates.io-index"4837source = "registry+https://github.com/rust-lang/crates.io-index"3470checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"4838checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"3471dependencies = [4839dependencies = [4840 "aws-lc-rs",3472 "bytes",4841 "bytes",3473 "getrandom 0.3.4",4842 "getrandom 0.3.4",3474 "lru-slab",4843 "lru-slab",3547checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"4916checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"3548dependencies = [4917dependencies = [3549 "chacha20 0.10.0",4918 "chacha20 0.10.0",3550 "getrandom 0.4.2",4919 "getrandom 0.4.3",3551 "rand_core 0.10.1",4920 "rand_core 0.10.1",3552]4921]355349223595source = "registry+https://github.com/rust-lang/crates.io-index"4964source = "registry+https://github.com/rust-lang/crates.io-index"3596checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"4965checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"49664967[[package]]4968name = "rand_pcg"4969version = "0.10.2"4970source = "registry+https://github.com/rust-lang/crates.io-index"4971checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"4972dependencies = [4973 "rand_core 0.10.1",4974]359749753598[[package]]4976[[package]]3599name = "rand_xorshift"4977name = "rand_xorshift"3610source = "registry+https://github.com/rust-lang/crates.io-index"4988source = "registry+https://github.com/rust-lang/crates.io-index"3611checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"4989checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"3612dependencies = [4990dependencies = [3613 "bitflags",4991 "bitflags 2.13.0",3614]4992]361549933616[[package]]4994[[package]]3619source = "registry+https://github.com/rust-lang/crates.io-index"4997source = "registry+https://github.com/rust-lang/crates.io-index"3620checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7"4998checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7"3621dependencies = [4999dependencies = [3622 "bitflags",5000 "bitflags 2.13.0",3623]5001]362450023625[[package]]5003[[package]]3652checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"5030checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"365350313654[[package]]5032[[package]]3655name = "remowt-client"5033name = "remowt-agent"3656version = "0.1.9"5034version = "0.1.9"5035dependencies = [5036 "anyhow",5037 "bifrostlink",5038 "bifrostlink-ports",5039 "clap",5040 "futures",5041 "nix",5042 "remowt-endpoints",5043 "remowt-link-shared",5044 "remowt-plugin",5045 "remowt-polkit-shared",5046 "remowt-ui-prompt",5047 "tempfile",5048 "tokio",5049 "tokio-util",5050 "tracing",5051 "tracing-subscriber",5052 "uuid",5053 "zbus",5054 "zbus_polkit",5055]50565057[[package]]3657source = "registry+https://github.com/rust-lang/crates.io-index"5058name = "remowt-client"3658checksum = "af0ab07d961f14fa5c4933899a5253341e2044dec71db6e341f80b16beff2c20"5059version = "0.1.9"3659dependencies = [5060dependencies = [3660 "anyhow",5061 "anyhow",3661 "bifrostlink",5062 "bifrostlink",3662 "bytes",5063 "bytes",3663 "camino",5064 "camino",3664 "futures",5065 "futures",5066 "iroh",3665 "remowt-endpoints",5067 "remowt-endpoints",3666 "remowt-link-shared",5068 "remowt-link-shared",3667 "russh",5069 "russh",3678[[package]]5080[[package]]3679name = "remowt-endpoints"5081name = "remowt-endpoints"3680version = "0.1.9"5082version = "0.1.9"3681source = "registry+https://github.com/rust-lang/crates.io-index"3682checksum = "9a841eec11d4cc0593188062634f7ee34e87a2052a70c0fa40fbecb4aa8692c0"3683dependencies = [5083dependencies = [3684 "bifrostlink",5084 "bifrostlink",3685 "camino",5085 "camino",5086 "iroh",3686 "nix",5087 "nix",5088 "remowt-link-shared",3687 "serde",5089 "serde",3688 "tempfile",5090 "tempfile",3689 "thiserror 2.0.18",5091 "thiserror 2.0.18",369450963695[[package]]5097[[package]]3696name = "remowt-fleet"5098name = "remowt-fleet"3697version = "0.1.0"5099version = "0.1.9"3698dependencies = [5100dependencies = [3699 "anyhow",5101 "anyhow",3700 "bifrostlink",5102 "bifrostlink",3712[[package]]5114[[package]]3713name = "remowt-link-shared"5115name = "remowt-link-shared"3714version = "0.1.9"5116version = "0.1.9"3715source = "registry+https://github.com/rust-lang/crates.io-index"3716checksum = "d5036c5e2d5f47bb7ecb25542268f865c298c2b747d74269ff6b2b387574071a"3717dependencies = [5117dependencies = [5118 "anyhow",3718 "bifrostlink",5119 "bifrostlink",3719 "bytes",5120 "bytes",5121 "camino",5122 "futures",5123 "iroh",5124 "iroh-base",5125 "n0-watcher",5126 "noq-udp",3720 "remowt-ui-prompt",5127 "remowt-ui-prompt",3721 "serde",5128 "serde",3722 "serde_json",5129 "serde_json",3723 "thiserror 2.0.18",5130 "thiserror 2.0.18",3724 "tokio",5131 "tokio",5132 "tokio-util",3725 "tracing",5133 "tracing",3726]5134]372751353728[[package]]5136[[package]]3729name = "remowt-plugin"5137name = "remowt-plugin"3730version = "0.1.9"5138version = "0.1.9"3731source = "registry+https://github.com/rust-lang/crates.io-index"3732checksum = "5ca599e67dc675d8dd59abfe8f08679f7f3c6fd7aa84507ab426839985a142ab"3733dependencies = [5139dependencies = [3734 "anyhow",5140 "anyhow",3735 "bifrostlink",5141 "bifrostlink",374251483743[[package]]5149[[package]]3744name = "remowt-plugin-fleet"5150name = "remowt-plugin-fleet"3745version = "0.1.0"5151version = "0.1.9"3746dependencies = [5152dependencies = [3747 "anyhow",5153 "anyhow",3748 "remowt-fleet",5154 "remowt-fleet",3749 "remowt-plugin",5155 "remowt-plugin",3750]5156]375151573752[[package]]5158[[package]]3753name = "remowt-ui-prompt"5159name = "remowt-polkit-shared"3754version = "0.1.9"5160version = "0.1.9"5161dependencies = [5162 "nix",5163 "serde",5164 "zbus",5165]51665167[[package]]3755source = "registry+https://github.com/rust-lang/crates.io-index"5168name = "remowt-ssh"5169version = "0.1.9"3756checksum = "2610933566f9b057dc47145b7bfc5876c4139eb766fc2fe9f2ce60baf1856c8c"5170dependencies = [5171 "anyhow",5172 "clap",5173 "nix",5174 "remowt-client",5175 "remowt-link-shared",5176 "remowt-ui-prompt",5177 "tokio",5178 "tracing",5179 "tracing-subscriber",5180]51815182[[package]]5183name = "remowt-ui-prompt"5184version = "0.1.9"3757dependencies = [5185dependencies = [3758 "anyhow",5186 "anyhow",3759 "bifrostlink",5187 "bifrostlink",376751953768[[package]]5196[[package]]3769name = "reqwest"5197name = "reqwest"3770version = "0.12.28"5198version = "0.13.4"3771source = "registry+https://github.com/rust-lang/crates.io-index"5199source = "registry+https://github.com/rust-lang/crates.io-index"3772checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"5200checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"3773dependencies = [5201dependencies = [3774 "base64 0.22.1",5202 "base64 0.22.1",3775 "bytes",5203 "bytes",3788 "pin-project-lite",5216 "pin-project-lite",3789 "quinn",5217 "quinn",3790 "rustls",5218 "rustls",3791 "rustls-native-certs",3792 "rustls-pki-types",5219 "rustls-pki-types",3793 "serde",5220 "rustls-platform-verifier",3794 "serde_json",3795 "serde_urlencoded",3796 "sync_wrapper",5221 "sync_wrapper",3797 "tokio",5222 "tokio",3798 "tokio-rustls",5223 "tokio-rustls",5224 "tokio-util",3799 "tower",5225 "tower",3800 "tower-http",5226 "tower-http",3801 "tower-service",5227 "tower-service",3802 "url",5228 "url",3803 "wasm-bindgen",5229 "wasm-bindgen",3804 "wasm-bindgen-futures",5230 "wasm-bindgen-futures",5231 "wasm-streams",3805 "web-sys",5232 "web-sys",3806]5233]52345235[[package]]5236name = "resolv-conf"5237version = "0.7.6"5238source = "registry+https://github.com/rust-lang/crates.io-index"5239checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7"380752403808[[package]]5241[[package]]3809name = "rfc6979"5242name = "rfc6979"3829 "windows-sys 0.52.0",5262 "windows-sys 0.52.0",3830]5263]52645265[[package]]5266name = "rpassword"5267version = "6.0.1"5268source = "registry+https://github.com/rust-lang/crates.io-index"5269checksum = "2bf099a1888612545b683d2661a1940089f6c2e5a8e38979b2159da876bfd956"5270dependencies = [5271 "libc",5272 "serde",5273 "serde_json",5274 "winapi",5275]383152763832[[package]]5277[[package]]3833name = "rsa"5278name = "rsa"3875checksum = "bbf893f64684e58da8a68d56a5e84d1cf0440226274c515770fe267707a7d0b0"5320checksum = "bbf893f64684e58da8a68d56a5e84d1cf0440226274c515770fe267707a7d0b0"3876dependencies = [5321dependencies = [3877 "aes 0.9.1",5322 "aes 0.9.1",3878 "bitflags",5323 "bitflags 2.13.0",3879 "block-padding 0.4.2",5324 "block-padding 0.4.2",3880 "byteorder",5325 "byteorder",3881 "bytes",5326 "bytes",3889 "der 0.8.0",5334 "der 0.8.0",3890 "digest 0.11.3",5335 "digest 0.11.3",3891 "ecdsa",5336 "ecdsa",3892 "ed25519-dalek 3.0.0-rc.0",5337 "ed25519-dalek",3893 "elliptic-curve",5338 "elliptic-curve",3894 "enum_dispatch",5339 "enum_dispatch",3895 "flate2",5340 "flate2",3896 "futures",5341 "futures",3897 "generic-array 1.4.3",5342 "generic-array 1.4.3",3898 "getrandom 0.4.2",5343 "getrandom 0.4.3",3899 "ghash 0.6.0",5344 "ghash 0.6.0",3900 "hex-literal",5345 "hex-literal",3901 "hmac 0.13.0",5346 "hmac 0.13.0",3998 "proc-macro2",5443 "proc-macro2",3999 "quote",5444 "quote",4000 "rust-embed-utils",5445 "rust-embed-utils",4001 "syn 2.0.117",5446 "syn 2.0.118",4002 "walkdir",5447 "walkdir",4003]5448]400454494039source = "registry+https://github.com/rust-lang/crates.io-index"5484source = "registry+https://github.com/rust-lang/crates.io-index"4040checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"5485checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"4041dependencies = [5486dependencies = [4042 "bitflags",5487 "bitflags 2.13.0",4043 "errno",5488 "errno",4044 "libc",5489 "libc",4045 "linux-raw-sys 0.4.15",5490 "linux-raw-sys 0.4.15",4052source = "registry+https://github.com/rust-lang/crates.io-index"5497source = "registry+https://github.com/rust-lang/crates.io-index"4053checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"5498checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"4054dependencies = [5499dependencies = [4055 "bitflags",5500 "bitflags 2.13.0",4056 "errno",5501 "errno",4057 "libc",5502 "libc",4058 "linux-raw-sys 0.12.1",5503 "linux-raw-sys 0.12.1",4065source = "registry+https://github.com/rust-lang/crates.io-index"5510source = "registry+https://github.com/rust-lang/crates.io-index"4066checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"5511checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"4067dependencies = [5512dependencies = [5513 "aws-lc-rs",5514 "log",4068 "once_cell",5515 "once_cell",4069 "ring",5516 "ring",4070 "rustls-pki-types",5517 "rustls-pki-types",4095 "zeroize",5542 "zeroize",4096]5543]55445545[[package]]5546name = "rustls-platform-verifier"5547version = "0.7.0"5548source = "registry+https://github.com/rust-lang/crates.io-index"5549checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"5550dependencies = [5551 "core-foundation 0.10.1",5552 "core-foundation-sys",5553 "jni 0.22.4",5554 "log",5555 "once_cell",5556 "rustls",5557 "rustls-native-certs",5558 "rustls-platform-verifier-android",5559 "rustls-webpki",5560 "security-framework",5561 "security-framework-sys",5562 "webpki-root-certs",5563 "windows-sys 0.61.2",5564]55655566[[package]]5567name = "rustls-platform-verifier-android"5568version = "0.1.1"5569source = "registry+https://github.com/rust-lang/crates.io-index"5570checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"409755714098[[package]]5572[[package]]4099name = "rustls-webpki"5573name = "rustls-webpki"4100version = "0.103.13"5574version = "0.103.13"4101source = "registry+https://github.com/rust-lang/crates.io-index"5575source = "registry+https://github.com/rust-lang/crates.io-index"4102checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"5576checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"4103dependencies = [5577dependencies = [5578 "aws-lc-rs",4104 "ring",5579 "ring",4105 "rustls-pki-types",5580 "rustls-pki-types",4106 "untrusted",5581 "untrusted",4155 "windows-sys 0.61.2",5630 "windows-sys 0.61.2",4156]5631]56325633[[package]]5634name = "scoped-tls"5635version = "1.0.1"5636source = "registry+https://github.com/rust-lang/crates.io-index"5637checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"415756384158[[package]]5639[[package]]4159name = "scopeguard"5640name = "scopeguard"4219source = "registry+https://github.com/rust-lang/crates.io-index"5700source = "registry+https://github.com/rust-lang/crates.io-index"4220checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"5701checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"4221dependencies = [5702dependencies = [4222 "bitflags",5703 "bitflags 2.13.0",4223 "core-foundation",5704 "core-foundation 0.10.1",4224 "core-foundation-sys",5705 "core-foundation-sys",4225 "libc",5706 "libc",4226 "security-framework-sys",5707 "security-framework-sys",4236 "libc",5717 "libc",4237]5718]57195720[[package]]5721name = "seize"5722version = "0.5.1"5723source = "registry+https://github.com/rust-lang/crates.io-index"5724checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521"5725dependencies = [5726 "libc",5727 "windows-sys 0.61.2",5728]423857294239[[package]]5730[[package]]4240name = "self_cell"5731name = "self_cell"4257source = "registry+https://github.com/rust-lang/crates.io-index"5748source = "registry+https://github.com/rust-lang/crates.io-index"4258checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"5749checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"57505751[[package]]5752name = "send_wrapper"5753version = "0.6.0"5754source = "registry+https://github.com/rust-lang/crates.io-index"5755checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"425957564260[[package]]5757[[package]]4261name = "serde"5758name = "serde"4267 "serde_derive",5764 "serde_derive",4268]5765]57665767[[package]]5768name = "serde_bytes"5769version = "0.11.19"5770source = "registry+https://github.com/rust-lang/crates.io-index"5771checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"5772dependencies = [5773 "serde",5774 "serde_core",5775]426957764270[[package]]5777[[package]]4271name = "serde_core"5778name = "serde_core"4284dependencies = [5791dependencies = [4285 "proc-macro2",5792 "proc-macro2",4286 "quote",5793 "quote",4287 "syn 2.0.117",5794 "syn 2.0.118",4288]5795]428957964290[[package]]5797[[package]]4308dependencies = [5815dependencies = [4309 "proc-macro2",5816 "proc-macro2",4310 "quote",5817 "quote",4311 "syn 2.0.117",5818 "syn 2.0.118",4312]5819]43134314[[package]]4315name = "serde_urlencoded"4316version = "0.7.1"4317source = "registry+https://github.com/rust-lang/crates.io-index"4318checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"4319dependencies = [4320 "form_urlencoded",4321 "itoa",4322 "ryu",4323 "serde",4324]432558204326[[package]]5821[[package]]4327name = "serdect"5822name = "serdect"4344 "digest 0.11.3",5839 "digest 0.11.3",4345]5840]58415842[[package]]5843name = "sha1_smol"5844version = "1.0.1"5845source = "registry+https://github.com/rust-lang/crates.io-index"5846checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"434658474347[[package]]5848[[package]]4348name = "sha2"5849name = "sha2"4433source = "registry+https://github.com/rust-lang/crates.io-index"5934source = "registry+https://github.com/rust-lang/crates.io-index"4434checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"5935checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"59365937[[package]]5938name = "simd_cesu8"5939version = "1.1.1"5940source = "registry+https://github.com/rust-lang/crates.io-index"5941checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"5942dependencies = [5943 "rustc_version",5944 "simdutf8",5945]59465947[[package]]5948name = "simdutf8"5949version = "0.1.5"5950source = "registry+https://github.com/rust-lang/crates.io-index"5951checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"59525953[[package]]5954name = "simple-dns"5955version = "0.11.3"5956source = "registry+https://github.com/rust-lang/crates.io-index"5957checksum = "7a75cbde1bf934313596a004973e462f9a82caa814dcf1a5f507bdf51597eeb4"5958dependencies = [5959 "bitflags 2.13.0",5960]443559614436[[package]]5962[[package]]4437name = "slab"5963name = "slab"4455 "windows-sys 0.61.2",5981 "windows-sys 0.61.2",4456]5982]59835984[[package]]5985name = "sorted-index-buffer"5986version = "0.2.1"5987source = "registry+https://github.com/rust-lang/crates.io-index"5988checksum = "ea06cc588e43c632923a55450401b8f25e628131571d4e1baea1bdfdb2b5ed06"59895990[[package]]5991name = "spez"5992version = "0.1.2"5993source = "registry+https://github.com/rust-lang/crates.io-index"5994checksum = "c87e960f4dca2788eeb86bbdde8dd246be8948790b7618d656e68f9b720a86e8"5995dependencies = [5996 "proc-macro2",5997 "quote",5998 "syn 2.0.118",5999]445760004458[[package]]6001[[package]]4459name = "spin"6002name = "spin"4460version = "0.9.8"6003version = "0.9.8"4461source = "registry+https://github.com/rust-lang/crates.io-index"6004source = "registry+https://github.com/rust-lang/crates.io-index"4462checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"6005checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"60066007[[package]]6008name = "spin"6009version = "0.10.0"6010source = "registry+https://github.com/rust-lang/crates.io-index"6011checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"446360124464[[package]]6013[[package]]4465name = "spki"6014name = "spki"4487source = "registry+https://github.com/rust-lang/crates.io-index"6036source = "registry+https://github.com/rust-lang/crates.io-index"4488checksum = "10db6f219196a8528f9ec904d9d45cdad692d65b0e57e72be4dedd1c5fddce36"6037checksum = "10db6f219196a8528f9ec904d9d45cdad692d65b0e57e72be4dedd1c5fddce36"4489dependencies = [6038dependencies = [4490 "aead 0.6.0",6039 "aead 0.6.1",4491 "aes 0.9.1",6040 "aes 0.9.1",4492 "aes-gcm 0.11.0-rc.4",6041 "aes-gcm 0.11.0-rc.4",4493 "cbc 0.2.1",6042 "cbc 0.2.1",4525 "argon2",6074 "argon2",4526 "bcrypt-pbkdf 0.11.0",6075 "bcrypt-pbkdf 0.11.0",4527 "ctutils",6076 "ctutils",4528 "ed25519-dalek 3.0.0-rc.0",6077 "ed25519-dalek",4529 "hex",6078 "hex",4530 "hmac 0.13.0",6079 "hmac 0.13.0",4531 "p256",6080 "p256",4548source = "registry+https://github.com/rust-lang/crates.io-index"6097source = "registry+https://github.com/rust-lang/crates.io-index"4549checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"6098checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"60996100[[package]]6101name = "static_assertions"6102version = "1.1.0"6103source = "registry+https://github.com/rust-lang/crates.io-index"6104checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"455061054551[[package]]6106[[package]]4552name = "strsim"6107name = "strsim"4553version = "0.11.1"6108version = "0.11.1"4554source = "registry+https://github.com/rust-lang/crates.io-index"6109source = "registry+https://github.com/rust-lang/crates.io-index"4555checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"6110checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"61116112[[package]]6113name = "strum"6114version = "0.28.0"6115source = "registry+https://github.com/rust-lang/crates.io-index"6116checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"6117dependencies = [6118 "strum_macros",6119]61206121[[package]]6122name = "strum_macros"6123version = "0.28.0"6124source = "registry+https://github.com/rust-lang/crates.io-index"6125checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"6126dependencies = [6127 "heck",6128 "proc-macro2",6129 "quote",6130 "syn 2.0.118",6131]455661324557[[package]]6133[[package]]4558name = "subtle"6134name = "subtle"457361494574[[package]]6150[[package]]4575name = "syn"6151name = "syn"4576version = "2.0.117"6152version = "2.0.118"4577source = "registry+https://github.com/rust-lang/crates.io-index"6153source = "registry+https://github.com/rust-lang/crates.io-index"4578checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"6154checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"4579dependencies = [6155dependencies = [4580 "proc-macro2",6156 "proc-macro2",4581 "quote",6157 "quote",4599dependencies = [6175dependencies = [4600 "proc-macro2",6176 "proc-macro2",4601 "quote",6177 "quote",4602 "syn 2.0.117",6178 "syn 2.0.118",4603]6179]61806181[[package]]6182name = "system-configuration"6183version = "0.7.0"6184source = "registry+https://github.com/rust-lang/crates.io-index"6185checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"6186dependencies = [6187 "bitflags 2.13.0",6188 "core-foundation 0.9.4",6189 "system-configuration-sys",6190]61916192[[package]]6193name = "system-configuration-sys"6194version = "0.6.0"6195source = "registry+https://github.com/rust-lang/crates.io-index"6196checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"6197dependencies = [6198 "core-foundation-sys",6199 "libc",6200]460462014605[[package]]6202[[package]]4606name = "tabled"6203name = "tabled"4607version = "0.20.0"6204version = "0.21.0"4608source = "registry+https://github.com/rust-lang/crates.io-index"6205source = "registry+https://github.com/rust-lang/crates.io-index"4609checksum = "e39a2ee1fbcd360805a771e1b300f78cc88fec7b8d3e2f71cd37bbf23e725c7d"6206checksum = "b5dc662e6da844ad6e428ad16b57967c9d33c82e16bb1c258326c0c078605dff"4610dependencies = [6207dependencies = [4611 "papergrid",6208 "papergrid",4612 "tabled_derive",6209 "tabled_derive",4623 "proc-macro-error2",6220 "proc-macro-error2",4624 "proc-macro2",6221 "proc-macro2",4625 "quote",6222 "quote",4626 "syn 2.0.117",6223 "syn 2.0.118",4627]6224]62256226[[package]]6227name = "tagptr"6228version = "0.2.0"6229source = "registry+https://github.com/rust-lang/crates.io-index"6230checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"462862314629[[package]]6232[[package]]4630name = "tempfile"6233name = "tempfile"4633checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"6236checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"4634dependencies = [6237dependencies = [4635 "fastrand",6238 "fastrand",4636 "getrandom 0.4.2",6239 "getrandom 0.4.3",4637 "once_cell",6240 "once_cell",4638 "rustix 1.1.4",6241 "rustix 1.1.4",4639 "windows-sys 0.61.2",6242 "windows-sys 0.61.2",4677dependencies = [6280dependencies = [4678 "proc-macro2",6281 "proc-macro2",4679 "quote",6282 "quote",4680 "syn 2.0.117",6283 "syn 2.0.118",4681]6284]468262854683[[package]]6286[[package]]4686source = "registry+https://github.com/rust-lang/crates.io-index"6289source = "registry+https://github.com/rust-lang/crates.io-index"4687checksum = "944ad38adcbb71eaa682c56bceeb079e4ca82b4b3edc2a0fde5cb297b77dac8d"6290checksum = "944ad38adcbb71eaa682c56bceeb079e4ca82b4b3edc2a0fde5cb297b77dac8d"4688dependencies = [6291dependencies = [4689 "syn 2.0.117",6292 "syn 2.0.118",4690 "test-log-core",6293 "test-log-core",4691]6294]469262954725dependencies = [6328dependencies = [4726 "proc-macro2",6329 "proc-macro2",4727 "quote",6330 "quote",4728 "syn 2.0.117",6331 "syn 2.0.118",4729]6332]473063334731[[package]]6334[[package]]4736dependencies = [6339dependencies = [4737 "proc-macro2",6340 "proc-macro2",4738 "quote",6341 "quote",4739 "syn 2.0.117",6342 "syn 2.0.118",4740]6343]474163444742[[package]]6345[[package]]4755checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469"6358checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469"4756dependencies = [6359dependencies = [4757 "deranged",6360 "deranged",6361 "js-sys",6362 "libc",4758 "num-conv",6363 "num-conv",6364 "num_threads",4759 "powerfmt",6365 "powerfmt",4760 "serde_core",6366 "serde_core",4761 "time-core",6367 "time-core",4817 "signal-hook-registry",6423 "signal-hook-registry",4818 "socket2",6424 "socket2",4819 "tokio-macros",6425 "tokio-macros",6426 "tracing",4820 "windows-sys 0.61.2",6427 "windows-sys 0.61.2",4821]6428]482264294828dependencies = [6435dependencies = [4829 "proc-macro2",6436 "proc-macro2",4830 "quote",6437 "quote",4831 "syn 2.0.117",6438 "syn 2.0.118",4832]6439]483364404834[[package]]6441[[package]]4850 "futures-core",6457 "futures-core",4851 "pin-project-lite",6458 "pin-project-lite",4852 "tokio",6459 "tokio",6460 "tokio-util",4853]6461]485464624855[[package]]6463[[package]]4861 "bytes",6469 "bytes",4862 "futures-core",6470 "futures-core",4863 "futures-sink",6471 "futures-sink",6472 "futures-util",4864 "pin-project-lite",6473 "pin-project-lite",4865 "tokio",6474 "tokio",4866]6475]64766477[[package]]6478name = "tokio-websockets"6479version = "0.13.2"6480source = "registry+https://github.com/rust-lang/crates.io-index"6481checksum = "dad543404f98bfc969aeb71994105c592acfc6c43323fddcd016bb208d1c65cb"6482dependencies = [6483 "base64 0.22.1",6484 "bytes",6485 "futures-core",6486 "futures-sink",6487 "getrandom 0.4.3",6488 "http",6489 "httparse",6490 "rand 0.10.1",6491 "ring",6492 "rustls-pki-types",6493 "sha1_smol",6494 "simdutf8",6495 "tokio",6496 "tokio-rustls",6497 "tokio-util",6498]486764994868[[package]]6500[[package]]4869name = "toml"6501name = "toml"4942 "tonic",6574 "tonic",4943]6575]65766577[[package]]6578name = "tonic-types"6579version = "0.14.6"6580source = "registry+https://github.com/rust-lang/crates.io-index"6581checksum = "73ab1b02061f83d519bba3caa167f88f261ef05720ab8ebc954ade70de3348e8"6582dependencies = [6583 "prost",6584 "prost-types",6585 "tonic",6586]494465874945[[package]]6588[[package]]4946name = "tower"6589name = "tower"4967source = "registry+https://github.com/rust-lang/crates.io-index"6610source = "registry+https://github.com/rust-lang/crates.io-index"4968checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"6611checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"4969dependencies = [6612dependencies = [4970 "bitflags",6613 "bitflags 2.13.0",4971 "bytes",6614 "bytes",4972 "futures-util",6615 "futures-util",4973 "http",6616 "http",4997source = "registry+https://github.com/rust-lang/crates.io-index"6640source = "registry+https://github.com/rust-lang/crates.io-index"4998checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"6641checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"4999dependencies = [6642dependencies = [6643 "log",5000 "pin-project-lite",6644 "pin-project-lite",5001 "tracing-attributes",6645 "tracing-attributes",5002 "tracing-core",6646 "tracing-core",5010dependencies = [6654dependencies = [5011 "proc-macro2",6655 "proc-macro2",5012 "quote",6656 "quote",5013 "syn 2.0.117",6657 "syn 2.0.118",5014]6658]501566595016[[package]]6660[[package]]504866925049[[package]]6693[[package]]5050name = "tracing-opentelemetry"6694name = "tracing-opentelemetry"5051version = "0.32.1"6695version = "0.33.0"5052source = "registry+https://github.com/rust-lang/crates.io-index"6696source = "registry+https://github.com/rust-lang/crates.io-index"5053checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc"6697checksum = "adbc64cba7137545b8044cb1fe9814f7aacf3c6b5f9b45be8bb5db538befdb26"5054dependencies = [6698dependencies = [5055 "js-sys",6699 "js-sys",5056 "opentelemetry",6700 "opentelemetry",5149source = "registry+https://github.com/rust-lang/crates.io-index"6793source = "registry+https://github.com/rust-lang/crates.io-index"5150checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"6794checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"67956796[[package]]6797name = "unicode-segmentation"6798version = "1.13.3"6799source = "registry+https://github.com/rust-lang/crates.io-index"6800checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"515168015152[[package]]6802[[package]]5153name = "unicode-width"6803name = "unicode-width"5209 "idna",6859 "idna",5210 "percent-encoding",6860 "percent-encoding",5211 "serde",6861 "serde",6862 "serde_derive",5212]6863]521368645214[[package]]6865[[package]]5229source = "registry+https://github.com/rust-lang/crates.io-index"6880source = "registry+https://github.com/rust-lang/crates.io-index"5230checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"6881checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"5231dependencies = [6882dependencies = [5232 "getrandom 0.4.2",6883 "getrandom 0.4.3",5233 "js-sys",6884 "js-sys",5234 "serde_core",6885 "serde_core",5235 "wasm-bindgen",6886 "wasm-bindgen",5241source = "registry+https://github.com/rust-lang/crates.io-index"6892source = "registry+https://github.com/rust-lang/crates.io-index"5242checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"6893checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"68946895[[package]]6896name = "vergen"6897version = "9.1.0"6898source = "registry+https://github.com/rust-lang/crates.io-index"6899checksum = "b849a1f6d8639e8de261e81ee0fc881e3e3620db1af9f2e0da015d4382ceaf75"6900dependencies = [6901 "anyhow",6902 "derive_builder",6903 "rustversion",6904 "vergen-lib",6905]69066907[[package]]6908name = "vergen-gitcl"6909version = "9.1.0"6910source = "registry+https://github.com/rust-lang/crates.io-index"6911checksum = "77ff3b5300a085d6bcd8fc96a507f706a28ae3814693236c9b409db71a1d15b9"6912dependencies = [6913 "anyhow",6914 "derive_builder",6915 "rustversion",6916 "time",6917 "vergen",6918 "vergen-lib",6919]69206921[[package]]6922name = "vergen-lib"6923version = "9.1.0"6924source = "registry+https://github.com/rust-lang/crates.io-index"6925checksum = "b34a29ba7e9c59e62f229ae1932fb1b8fb8a6fdcc99215a641913f5f5a59a569"6926dependencies = [6927 "anyhow",6928 "derive_builder",6929 "rustversion",6930]524369315244[[package]]6932[[package]]5245name = "version_check"6933name = "version_check"5265checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd"6953checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd"5266dependencies = [6954dependencies = [5267 "arrayvec",6955 "arrayvec",5268 "bitflags",6956 "bitflags 2.13.0",5269 "cursor-icon",6957 "cursor-icon",5270 "log",6958 "log",5271 "memchr",6959 "memchr",5302source = "registry+https://github.com/rust-lang/crates.io-index"6990source = "registry+https://github.com/rust-lang/crates.io-index"5303checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"6991checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"5304dependencies = [6992dependencies = [5305 "wit-bindgen 0.57.1",6993 "wit-bindgen",5306]6994]53075308[[package]]5309name = "wasip3"5310version = "0.4.0+wasi-0.3.0-rc-2026-01-06"5311source = "registry+https://github.com/rust-lang/crates.io-index"5312checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"5313dependencies = [5314 "wit-bindgen 0.51.0",5315]531669955317[[package]]6996[[package]]5318name = "wasite"6997name = "wasite"5362 "bumpalo",7041 "bumpalo",5363 "proc-macro2",7042 "proc-macro2",5364 "quote",7043 "quote",5365 "syn 2.0.117",7044 "syn 2.0.118",5366 "wasm-bindgen-shared",7045 "wasm-bindgen-shared",5367]7046]536870475375 "unicode-ident",7054 "unicode-ident",5376]7055]53775378[[package]]5379name = "wasm-encoder"5380version = "0.244.0"5381source = "registry+https://github.com/rust-lang/crates.io-index"5382checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"5383dependencies = [5384 "leb128fmt",5385 "wasmparser",5386]538770565388[[package]]7057[[package]]5389name = "wasm-metadata"7058name = "wasm-streams"5390version = "0.244.0"7059version = "0.5.0"5391source = "registry+https://github.com/rust-lang/crates.io-index"7060source = "registry+https://github.com/rust-lang/crates.io-index"5392checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"7061checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"5393dependencies = [7062dependencies = [5394 "anyhow",7063 "futures-util",5395 "indexmap",7064 "js-sys",5396 "wasm-encoder",7065 "wasm-bindgen",5397 "wasmparser",7066 "wasm-bindgen-futures",7067 "web-sys",5398]7068]53995400[[package]]5401name = "wasmparser"5402version = "0.244.0"5403source = "registry+https://github.com/rust-lang/crates.io-index"5404checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"5405dependencies = [5406 "bitflags",5407 "hashbrown 0.15.5",5408 "indexmap",5409 "semver",5410]541170695412[[package]]7070[[package]]5413name = "web-sys"7071name = "web-sys"5429 "wasm-bindgen",7087 "wasm-bindgen",5430]7088]70897090[[package]]7091name = "webpki-root-certs"7092version = "1.0.8"7093source = "registry+https://github.com/rust-lang/crates.io-index"7094checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267"7095dependencies = [7096 "rustls-pki-types",7097]70987099[[package]]7100name = "webpki-roots"7101version = "1.0.8"7102source = "registry+https://github.com/rust-lang/crates.io-index"7103checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf"7104dependencies = [7105 "rustls-pki-types",7106]543171075432[[package]]7108[[package]]5433name = "which"7109name = "which"5452 "web-sys",7128 "web-sys",5453]7129]71307131[[package]]7132name = "widestring"7133version = "1.2.1"7134source = "registry+https://github.com/rust-lang/crates.io-index"7135checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"71367137[[package]]7138name = "winapi"7139version = "0.3.9"7140source = "registry+https://github.com/rust-lang/crates.io-index"7141checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"7142dependencies = [7143 "winapi-i686-pc-windows-gnu",7144 "winapi-x86_64-pc-windows-gnu",7145]71467147[[package]]7148name = "winapi-i686-pc-windows-gnu"7149version = "0.4.0"7150source = "registry+https://github.com/rust-lang/crates.io-index"7151checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"545471525455[[package]]7153[[package]]5456name = "winapi-util"7154name = "winapi-util"5461 "windows-sys 0.61.2",7159 "windows-sys 0.61.2",5462]7160]71617162[[package]]7163name = "winapi-x86_64-pc-windows-gnu"7164version = "0.4.0"7165source = "registry+https://github.com/rust-lang/crates.io-index"7166checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"546371675464[[package]]7168[[package]]5465name = "windows"7169name = "windows"5514dependencies = [7218dependencies = [5515 "proc-macro2",7219 "proc-macro2",5516 "quote",7220 "quote",5517 "syn 2.0.117",7221 "syn 2.0.118",5518]7222]551972235520[[package]]7224[[package]]5525dependencies = [7229dependencies = [5526 "proc-macro2",7230 "proc-macro2",5527 "quote",7231 "quote",5528 "syn 2.0.117",7232 "syn 2.0.118",5529]7233]553072345531[[package]]7235[[package]]5544 "windows-link",7248 "windows-link",5545]7249]72507251[[package]]7252name = "windows-registry"7253version = "0.6.1"7254source = "registry+https://github.com/rust-lang/crates.io-index"7255checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"7256dependencies = [7257 "windows-link",7258 "windows-result",7259 "windows-strings",7260]554672615547[[package]]7262[[package]]5548name = "windows-result"7263name = "windows-result"5562 "windows-link",7277 "windows-link",5563]7278]72797280[[package]]7281name = "windows-sys"7282version = "0.45.0"7283source = "registry+https://github.com/rust-lang/crates.io-index"7284checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"7285dependencies = [7286 "windows-targets 0.42.2",7287]556472885565[[package]]7289[[package]]5566name = "windows-sys"7290name = "windows-sys"5598 "windows-link",7322 "windows-link",5599]7323]73247325[[package]]7326name = "windows-targets"7327version = "0.42.2"7328source = "registry+https://github.com/rust-lang/crates.io-index"7329checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"7330dependencies = [7331 "windows_aarch64_gnullvm 0.42.2",7332 "windows_aarch64_msvc 0.42.2",7333 "windows_i686_gnu 0.42.2",7334 "windows_i686_msvc 0.42.2",7335 "windows_x86_64_gnu 0.42.2",7336 "windows_x86_64_gnullvm 0.42.2",7337 "windows_x86_64_msvc 0.42.2",7338]560073395601[[package]]7340[[package]]5602name = "windows-targets"7341name = "windows-targets"5640 "windows-link",7379 "windows-link",5641]7380]73817382[[package]]7383name = "windows_aarch64_gnullvm"7384version = "0.42.2"7385source = "registry+https://github.com/rust-lang/crates.io-index"7386checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"564273875643[[package]]7388[[package]]5644name = "windows_aarch64_gnullvm"7389name = "windows_aarch64_gnullvm"5652source = "registry+https://github.com/rust-lang/crates.io-index"7397source = "registry+https://github.com/rust-lang/crates.io-index"5653checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"7398checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"73997400[[package]]7401name = "windows_aarch64_msvc"7402version = "0.42.2"7403source = "registry+https://github.com/rust-lang/crates.io-index"7404checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"565474055655[[package]]7406[[package]]5656name = "windows_aarch64_msvc"7407name = "windows_aarch64_msvc"5664source = "registry+https://github.com/rust-lang/crates.io-index"7415source = "registry+https://github.com/rust-lang/crates.io-index"5665checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"7416checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"74177418[[package]]7419name = "windows_i686_gnu"7420version = "0.42.2"7421source = "registry+https://github.com/rust-lang/crates.io-index"7422checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"566674235667[[package]]7424[[package]]5668name = "windows_i686_gnu"7425name = "windows_i686_gnu"5688source = "registry+https://github.com/rust-lang/crates.io-index"7445source = "registry+https://github.com/rust-lang/crates.io-index"5689checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"7446checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"74477448[[package]]7449name = "windows_i686_msvc"7450version = "0.42.2"7451source = "registry+https://github.com/rust-lang/crates.io-index"7452checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"569074535691[[package]]7454[[package]]5692name = "windows_i686_msvc"7455name = "windows_i686_msvc"5700source = "registry+https://github.com/rust-lang/crates.io-index"7463source = "registry+https://github.com/rust-lang/crates.io-index"5701checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"7464checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"74657466[[package]]7467name = "windows_x86_64_gnu"7468version = "0.42.2"7469source = "registry+https://github.com/rust-lang/crates.io-index"7470checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"570274715703[[package]]7472[[package]]5704name = "windows_x86_64_gnu"7473name = "windows_x86_64_gnu"5712source = "registry+https://github.com/rust-lang/crates.io-index"7481source = "registry+https://github.com/rust-lang/crates.io-index"5713checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"7482checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"74837484[[package]]7485name = "windows_x86_64_gnullvm"7486version = "0.42.2"7487source = "registry+https://github.com/rust-lang/crates.io-index"7488checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"571474895715[[package]]7490[[package]]5716name = "windows_x86_64_gnullvm"7491name = "windows_x86_64_gnullvm"5724source = "registry+https://github.com/rust-lang/crates.io-index"7499source = "registry+https://github.com/rust-lang/crates.io-index"5725checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"7500checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"75017502[[package]]7503name = "windows_x86_64_msvc"7504version = "0.42.2"7505source = "registry+https://github.com/rust-lang/crates.io-index"7506checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"572675075727[[package]]7508[[package]]5728name = "windows_x86_64_msvc"7509name = "windows_x86_64_msvc"5745 "memchr",7526 "memchr",5746]7527]57475748[[package]]5749name = "wit-bindgen"5750version = "0.51.0"5751source = "registry+https://github.com/rust-lang/crates.io-index"5752checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"5753dependencies = [5754 "wit-bindgen-rust-macro",5755]575675285757[[package]]7529[[package]]5758name = "wit-bindgen"7530name = "wit-bindgen"5759version = "0.57.1"7531version = "0.57.1"5760source = "registry+https://github.com/rust-lang/crates.io-index"7532source = "registry+https://github.com/rust-lang/crates.io-index"5761checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"7533checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"57625763[[package]]5764name = "wit-bindgen-core"5765version = "0.51.0"5766source = "registry+https://github.com/rust-lang/crates.io-index"5767checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"5768dependencies = [5769 "anyhow",5770 "heck",5771 "wit-parser",5772]57735774[[package]]5775name = "wit-bindgen-rust"5776version = "0.51.0"5777source = "registry+https://github.com/rust-lang/crates.io-index"5778checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"5779dependencies = [5780 "anyhow",5781 "heck",5782 "indexmap",5783 "prettyplease",5784 "syn 2.0.117",5785 "wasm-metadata",5786 "wit-bindgen-core",5787 "wit-component",5788]57895790[[package]]5791name = "wit-bindgen-rust-macro"5792version = "0.51.0"5793source = "registry+https://github.com/rust-lang/crates.io-index"5794checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"5795dependencies = [5796 "anyhow",5797 "prettyplease",5798 "proc-macro2",5799 "quote",5800 "syn 2.0.117",5801 "wit-bindgen-core",5802 "wit-bindgen-rust",5803]580475345805[[package]]7535[[package]]5806name = "wit-component"7536name = "wmi"5807version = "0.244.0"7537version = "0.18.4"5808source = "registry+https://github.com/rust-lang/crates.io-index"7538source = "registry+https://github.com/rust-lang/crates.io-index"5809checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"7539checksum = "7c81b85c57a57500e56669586496bf2abd5cf082b9d32995251185d105208b64"5810dependencies = [7540dependencies = [5811 "anyhow",7541 "chrono",5812 "bitflags",7542 "futures",5813 "indexmap",5814 "log",7543 "log",5815 "serde",7544 "serde",5816 "serde_derive",7545 "thiserror 2.0.18",5817 "serde_json",5818 "wasm-encoder",7546 "windows",5819 "wasm-metadata",5820 "wasmparser",5821 "wit-parser",7547 "windows-core",5822]7548]58235824[[package]]5825name = "wit-parser"5826version = "0.244.0"5827source = "registry+https://github.com/rust-lang/crates.io-index"5828checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"5829dependencies = [5830 "anyhow",5831 "id-arena",5832 "indexmap",5833 "log",5834 "semver",5835 "serde",5836 "serde_derive",5837 "serde_json",5838 "unicode-xid",5839 "wasmparser",5840]584175495842[[package]]7550[[package]]5843name = "writeable"7551name = "writeable"5844version = "0.6.3"7552version = "0.6.3"5845source = "registry+https://github.com/rust-lang/crates.io-index"7553source = "registry+https://github.com/rust-lang/crates.io-index"5846checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"7554checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"75557556[[package]]7557name = "ws_stream_wasm"7558version = "0.7.5"7559source = "registry+https://github.com/rust-lang/crates.io-index"7560checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc"7561dependencies = [7562 "async_io_stream",7563 "futures",7564 "js-sys",7565 "log",7566 "pharos",7567 "rustc_version",7568 "send_wrapper",7569 "thiserror 2.0.18",7570 "wasm-bindgen",7571 "wasm-bindgen-futures",7572 "web-sys",7573]584775745848[[package]]7575[[package]]5849name = "wsl"7576name = "wsl"5863 "zeroize",7590 "zeroize",5864]7591]75927593[[package]]7594name = "xml-rs"7595version = "0.8.28"7596source = "registry+https://github.com/rust-lang/crates.io-index"7597checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"75987599[[package]]7600name = "xmltree"7601version = "0.10.3"7602source = "registry+https://github.com/rust-lang/crates.io-index"7603checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb"7604dependencies = [7605 "xml-rs",7606]586576075866[[package]]7608[[package]]5867name = "yoke"7609name = "yoke"5882dependencies = [7624dependencies = [5883 "proc-macro2",7625 "proc-macro2",5884 "quote",7626 "quote",5885 "syn 2.0.117",7627 "syn 2.0.118",5886 "synstructure",7628 "synstructure",5887]7629]588876305911 "rustix 1.1.4",7653 "rustix 1.1.4",5912 "serde",7654 "serde",5913 "serde_repr",7655 "serde_repr",7656 "tokio",5914 "tracing",7657 "tracing",5915 "uds_windows",7658 "uds_windows",5916 "uuid",7659 "uuid",5930 "proc-macro-crate",7673 "proc-macro-crate",5931 "proc-macro2",7674 "proc-macro2",5932 "quote",7675 "quote",5933 "syn 2.0.117",7676 "syn 2.0.118",5934 "zbus_names",7677 "zbus_names",5935 "zvariant",7678 "zvariant",5936 "zvariant_utils",7679 "zvariant_utils",5947 "zvariant",7690 "zvariant",5948]7691]76927693[[package]]7694name = "zbus_polkit"7695version = "5.0.0"7696source = "registry+https://github.com/rust-lang/crates.io-index"7697checksum = "ad23d5c4d198c7e2641b33e6e0d1f866f117408ba66fe80bbe52e289eeb77c52"7698dependencies = [7699 "enumflags2",7700 "serde",7701 "serde_repr",7702 "static_assertions",7703 "zbus",7704]594977055950[[package]]7706[[package]]5951name = "zerocopy"7707name = "zerocopy"5964dependencies = [7720dependencies = [5965 "proc-macro2",7721 "proc-macro2",5966 "quote",7722 "quote",5967 "syn 2.0.117",7723 "syn 2.0.118",5968]7724]596977255970[[package]]7726[[package]]5984dependencies = [7740dependencies = [5985 "proc-macro2",7741 "proc-macro2",5986 "quote",7742 "quote",5987 "syn 2.0.117",7743 "syn 2.0.118",5988 "synstructure",7744 "synstructure",5989]7745]599077466005dependencies = [7761dependencies = [6006 "proc-macro2",7762 "proc-macro2",6007 "quote",7763 "quote",6008 "syn 2.0.117",7764 "syn 2.0.118",6009]7765]601077666011[[package]]7767[[package]]6039dependencies = [7795dependencies = [6040 "proc-macro2",7796 "proc-macro2",6041 "quote",7797 "quote",6042 "syn 2.0.117",7798 "syn 2.0.118",6043]7799]604478006045[[package]]7801[[package]]6071 "proc-macro-crate",7827 "proc-macro-crate",6072 "proc-macro2",7828 "proc-macro2",6073 "quote",7829 "quote",6074 "syn 2.0.117",7830 "syn 2.0.118",6075 "zvariant_utils",7831 "zvariant_utils",6076]7832]607778336084 "proc-macro2",7840 "proc-macro2",6085 "quote",7841 "quote",6086 "serde",7842 "serde",6087 "syn 2.0.117",7843 "syn 2.0.118",6088 "winnow",7844 "winnow",6089]7845]60907846Cargo.tomldiffbeforeafterboth--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,9 +1,10 @@
[workspace]
-members = ["crates/*", "cmds/*"]
-resolver = "2"
-package.version = "0.1.0"
+members = ["crates/*", "cmds/*", "remowt/crates/*", "remowt/cmds/*"]
+resolver = "3"
+package.version = "0.1.9"
package.edition = "2024"
-package.rust-version = "1.89.0"
+package.rust-version = "1.95.0"
+package.license = "MIT"
[workspace.dependencies]
fleet-base = { path = "./crates/fleet-base" }
@@ -13,42 +14,56 @@
opentelemetry-exporter-env = { path = "./crates/opentelemetry-exporter-env" }
remowt-fleet = { path = "./crates/remowt-fleet" }
-remowt-client = "0.1.3"
-remowt-endpoints = "0.1.3"
-remowt-link-shared = "0.1.3"
-remowt-plugin = "0.1.3"
-remowt-ui-prompt = "0.1.3"
+remowt-client = { version = "0.1.9", path = "remowt/crates/remowt-client" }
+remowt-polkit-shared = { version = "0.1.9", path = "remowt/crates/polkit-shared" }
+remowt-link-shared = { version = "0.1.9", path = "remowt/crates/remowt-link-shared" }
+remowt-plugin = { version = "0.1.9", path = "remowt/crates/remowt-plugin" }
+remowt-ui-prompt = { version = "0.1.9", path = "remowt/crates/remowt-ui-prompt" }
+remowt-endpoints = { version = "0.1.9", path = "remowt/crates/remowt-endpoints" }
-bifrostlink = "0.2.3"
+bifrostlink = "0.2.0"
+bifrostlink-macros = "0.2.0"
+bifrostlink-ports = "0.2.0"
-uuid = { version = "1", features = ["v4"] }
+iroh = { version = "1.0.0", features = ["unstable-custom-transports"] }
+iroh-base = "1.0.0"
+noq-udp = { version = "1.0.0", default-features = false }
+n0-watcher = "1.0.0"
+uuid = { version = "1", features = ["v4"] }
+russh = { version = "0.61.2", default-features = false, features = [
+ "ring",
+ "flate2",
+ "rsa",
+] }
+russh-config = "0.58.0"
age = { version = "0.11", features = ["plugin", "ssh"] }
anyhow = "1.0"
base64 = "0.22.1"
bindgen = "0.72.0"
+bytes = "1.11.0"
camino = "1.2.2"
chrono = { version = "0.4.41", features = ["serde"] }
clap = { version = "4.5", features = ["derive", "env", "unicode", "wrap_help"] }
clap_complete = "4.5"
cxx = "1.0.168"
cxx-build = "1.0.168"
-ed25519-dalek = "2.1"
+ed25519-dalek = "3.0.0-rc.0"
futures = "0.3.31"
hex = "0.4.3"
-hmac = "0.12"
+hmac = "0.13.0"
hostname = "0.4.1"
human-repr = "1.1"
indicatif = "0.18"
indoc = "2.0.6"
-itertools = "0.14.0"
+itertools = "0.15.0"
linked-hash-map = "0.5.6"
nix = { version = "0.31.2", features = ["fs", "user"] }
nom = "8.0.0"
-opentelemetry = "0.31.0"
-opentelemetry-appender-tracing = "0.31.1"
-opentelemetry-otlp = { version = "0.31.0", features = ["grpc-tonic", "gzip-tonic", "http-json", "reqwest-rustls"] }
-opentelemetry_sdk = "0.31.0"
+opentelemetry = "0.32.0"
+opentelemetry-appender-tracing = "0.32.0"
+opentelemetry-otlp = { version = "0.32.0", features = ["grpc-tonic", "gzip-tonic", "http-json", "reqwest-rustls"] }
+opentelemetry_sdk = "0.32.0"
pbkdf2 = "0.12"
peg = "0.8.5"
pkg-config = "0.3.30"
@@ -56,8 +71,8 @@
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha2 = "0.10"
-shlex = "1.3"
-tabled = "0.20.0"
+shlex = "2.0.1"
+tabled = "0.21.0"
tempfile = "3.20"
test-log = { version = "0.2.19", features = ["trace"] }
thiserror = "2.0.12"
@@ -65,12 +80,16 @@
tokio = { version = "1.45.1", features = ["fs", "macros", "rt", "rt-multi-thread", "sync", "time"] }
tracing = "0.1"
tracing-indicatif = "0.3.13"
-tracing-opentelemetry = "0.32.1"
+tracing-opentelemetry = "0.33.0"
# For fixed coloring needs to be updated to https://github.com/tokio-rs/tracing/pull/3484
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "fmt"] }
unicode_categories = "0.1.1"
vte = { version = "0.15.0", features = ["ansi"] }
x25519-dalek = { version = "2.0.1", features = ["getrandom"] }
+zbus = "5.16.0"
+zbus_polkit = "5.0.0"
+tokio-util = "0.7.11"
+pam-client = "0.5.0"
[profile.dev]
panic = "abort"
cmds/remowt-plugin-fleet/src/main.rsdiffbeforeafterboth--- a/cmds/remowt-plugin-fleet/src/main.rs
+++ b/cmds/remowt-plugin-fleet/src/main.rs
@@ -1,4 +1,4 @@
-use remowt_fleet::{Nix, init_libraries};
+use remowt_fleet::{init_libraries, Nix};
fn main() -> anyhow::Result<()> {
remowt_plugin::run(|rpc| {
crates/fleet-base/src/host.rsdiffbeforeafterboth--- a/crates/fleet-base/src/host.rs
+++ b/crates/fleet-base/src/host.rs
@@ -425,9 +425,9 @@
warn!("failed to sign store paths: {e}");
}
let store = self.nix_store().await?;
+ let eval_store = eval_store();
{
let path = path.clone();
- let eval_store = eval_store();
spawn_blocking(move || eval_store.copy_to(&store, path.as_ref()))
.await
.expect("copy_to panicked")
crates/nix-eval/src/lib.rsdiffbeforeafterboth--- a/crates/nix-eval/src/lib.rs
+++ b/crates/nix-eval/src/lib.rs
@@ -565,7 +565,7 @@
#[instrument(skip(self, dst))]
pub fn copy_to(&self, dst: &Store, path: &Utf8Path) -> Result<()> {
let sp = self
- .parse_path(&path)
+ .parse_path(path)
.context("failed to parse store path")?;
let rc = with_default_context(|c, _| unsafe {
store_copy_closure(c, self.as_ptr(), dst.0, sp.as_ptr())
remowt/README.adocdiffbeforeafterboth--- /dev/null
+++ b/remowt/README.adoc
@@ -0,0 +1,3 @@
+= Remowt
+
+A framework for executing code on remote machines.
remowt/cmds/polkit-dbus-helper/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/polkit-dbus-helper/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "polkit-backend"
+description = "polkit/PAM D-Bus authentication backend for remowt"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+clap = { workspace = true, features = ["derive"] }
+nix.workspace = true
+pam-client.workspace = true
+remowt-polkit-shared.workspace = true
+tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] }
+tracing.workspace = true
+tracing-subscriber.workspace = true
+remowt-ui-prompt.workspace = true
+zbus = { workspace = true, features = ["tokio"] }
+zbus_polkit = { workspace = true, features = ["tokio"] }
remowt/cmds/polkit-dbus-helper/README.adocdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/polkit-dbus-helper/README.adoc
@@ -0,0 +1,8 @@
+== What is it?
+
+Usually, there is a `polkit-agent-helper-1` suid binary installed on the system with polkit package.
+
+This, however, an alternative to that approach, a system daemon listening for dbus requests, which works
+without using suid binaries.
+
+In future it will provide some additional features.
remowt/cmds/polkit-dbus-helper/etc/systemd/system/remowt-polkit-helper.servicediffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/polkit-dbus-helper/etc/systemd/system/remowt-polkit-helper.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=Remowt polkit helper service
+
+[Service]
+Type=dbus
+BusName=lach.polkit.helper1
+ExecStart=@libexecdir@/polkit-backend
+# TODO: Hardening
+
+[Install]
+WantedBy=multi-user.target
+Alias=dbus-lach.polkit.helper1.service
remowt/cmds/polkit-dbus-helper/share/dbus-1/system-services/lach.polkit.helper1.confdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/polkit-dbus-helper/share/dbus-1/system-services/lach.polkit.helper1.conf
@@ -0,0 +1,5 @@
+[D-BUS Service]
+Name=lach.polkit.helper1
+Exec=/bin/false
+User=root
+SystemdService=dbus-lach.polkit.helper1.service
remowt/cmds/polkit-dbus-helper/share/dbus-1/system.d/lach.polkit.helper1.confdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/polkit-dbus-helper/share/dbus-1/system.d/lach.polkit.helper1.conf
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" "https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+<busconfig>
+ <policy user="root">
+ <allow own = "lach.polkit.helper1"/>
+ <allow send_interface="lach.PolkitInputHandler"/>
+ </policy>
+ <policy context="default">
+ <allow send_destination="lach.polkit.helper1"/>
+ <deny send_interface="lach.PolkitInputHandler"/>
+ </policy>
+</busconfig>
remowt/cmds/polkit-dbus-helper/src/main.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/polkit-dbus-helper/src/main.rs
@@ -0,0 +1,236 @@
+use std::collections::{HashMap, HashSet};
+use std::ffi::{CStr, CString};
+use std::future::pending;
+use std::sync::LazyLock;
+
+use anyhow::Context as _;
+use clap::Parser;
+use nix::unistd::{setuid, Uid, User};
+use pam_client::{Context, ConversationHandler, ErrorCode, Flag};
+use remowt_polkit_shared::BackendRequest;
+use remowt_ui_prompt::dbus::DbusPrompterProxyBlocking;
+use remowt_ui_prompt::BlockingPrompter;
+use tokio::task::{block_in_place, spawn_blocking};
+use tracing::trace;
+use zbus::fdo;
+use zbus::message::Header;
+use zbus::zvariant::OwnedValue;
+use zbus::{blocking, interface, proxy, Connection};
+
+struct Helper {
+ connection: Connection,
+ blocking_connection: blocking::Connection,
+}
+
+static ALLOWED_ENVIRONMENT: LazyLock<HashSet<&str>> = LazyLock::new(|| {
+ [
+ // pam ssh agent auth
+ "SSH_AUTH_SOCK",
+ // ssh itself provides this when running PAM
+ "SSH_AUTH_INFO_0",
+ // contains user which ran sudo
+ "SUDO_USER",
+ ]
+ .into_iter()
+ .collect()
+});
+
+struct Conversation<P>(P);
+impl<P: BlockingPrompter> Conversation<P> {
+ fn prompt_inner(&self, echo: bool, prompt: &CStr) -> Result<CString, ErrorCode> {
+ trace!("do prompt");
+ let out = self
+ .0
+ .prompt_text(echo, &prompt.to_string_lossy(), "PAM prompt request", &[])
+ .map_err(|e| {
+ trace!("prompt error: {e}");
+ ErrorCode::CONV_ERR
+ })?;
+ CString::new(out).map_err(|_| ErrorCode::CONV_AGAIN)
+ }
+ fn text_inner(&self, error: bool, msg: &CStr) {
+ trace!("do text");
+ let msg = msg.to_string_lossy();
+ let _ = self.0.display_text(error, &msg, &[]);
+ }
+}
+impl<P: BlockingPrompter> ConversationHandler for Conversation<P> {
+ fn prompt_echo_on(&mut self, prompt: &CStr) -> Result<CString, ErrorCode> {
+ self.prompt_inner(true, prompt)
+ }
+
+ fn prompt_echo_off(&mut self, prompt: &CStr) -> Result<CString, ErrorCode> {
+ self.prompt_inner(false, prompt)
+ }
+
+ fn text_info(&mut self, msg: &CStr) {
+ self.text_inner(false, msg)
+ }
+
+ fn error_msg(&mut self, msg: &CStr) {
+ self.text_inner(true, msg)
+ }
+
+ fn radio_prompt(&mut self, prompt: &CStr) -> Result<bool, ErrorCode> {
+ let prompt = prompt.to_string_lossy();
+ let result = self
+ .0
+ .prompt_radio(&prompt, "PAM prompt request", &[])
+ .map_err(|_| ErrorCode::CONV_ERR)?;
+ Ok(result)
+ }
+}
+
+#[proxy(
+ default_service = "org.freedesktop.DBus",
+ default_path = "/org/freedesktop/DBus"
+)]
+trait DBus {
+ fn get_connection_credentials(&self, body: &str) -> zbus::Result<HashMap<String, OwnedValue>>;
+}
+
+#[interface(name = "lach.PolkitHelper")]
+impl Helper {
+ async fn init_conversation(
+ &self,
+ request: BackendRequest,
+ #[zbus(header)] hdr: Header<'_>,
+ ) -> fdo::Result<()> {
+ let Some(sender) = hdr.sender().map(|v| v.to_owned()) else {
+ trace!("missing sender");
+ return Err(fdo::Error::AuthFailed("missing sender".to_owned()));
+ };
+
+ let dbus = DBusProxy::new(&self.connection).await?;
+
+ // TOCTOU: sender might be already disconnected, and there might be another
+ // user with different user id here, but does it matters?
+ let reply = dbus.get_connection_credentials(&sender).await?;
+ let connection_uid: u32 = (&reply["UnixUserID"]).try_into().unwrap();
+
+ let identity = request.identity.clone();
+ let blocking_connection = self.blocking_connection.clone();
+ let thread_result: fdo::Result<()> = block_in_place(move || {
+ trace!("find user");
+ let Some(identity_uid) = identity.uid() else {
+ return Err(fdo::Error::AuthFailed("can't process identity".to_owned()));
+ };
+ let user = User::from_uid(identity_uid)
+ .map_err(|_| fdo::Error::AuthFailed("error querying user".to_owned()))?
+ .ok_or_else(|| fdo::Error::AuthFailed("uid not found".to_owned()))?;
+
+ let responder = DbusPrompterProxyBlocking::new(
+ &blocking_connection,
+ sender,
+ request.prompter_path,
+ )?;
+ let conversation = Conversation(responder);
+ trace!("run context for {}", &user.name);
+ let mut ctx = Context::new(
+ // TODO: Should another scope be used?
+ "login",
+ Some(&user.name),
+ conversation,
+ )
+ .map_err(|_| fdo::Error::Failed("pam context init failed".to_owned()))?;
+
+ trace!("fill env");
+ for (k, v) in request.environment {
+ if k.contains('=') || !ALLOWED_ENVIRONMENT.contains(k.as_str()) {
+ continue;
+ }
+ let _ = ctx.putenv(format!("{k}={v}"));
+ }
+
+ trace!("authenticate");
+ ctx.authenticate(Flag::NONE)
+ .map_err(|_| fdo::Error::AuthFailed("pam authentication failed".to_owned()))?;
+
+ trace!("acct mgmt");
+ ctx.acct_mgmt(Flag::NONE)
+ .map_err(|_| fdo::Error::AuthFailed("pam acct mgmt failed".to_owned()))?;
+
+ Ok(())
+ });
+
+ thread_result?;
+
+ trace!("respond");
+ let proxy = zbus_polkit::policykit1::AuthorityProxy::new(&self.connection).await?;
+
+ let identity_details = request
+ .identity
+ .details
+ .iter()
+ .map(|(k, v)| (k.as_str(), (**v).try_clone().expect("success")))
+ .collect::<HashMap<_, _>>();
+ proxy
+ .authentication_agent_response2(
+ connection_uid,
+ &request.cookie,
+ &zbus_polkit::policykit1::Identity {
+ identity_kind: &request.identity.kind,
+ identity_details: &identity_details,
+ },
+ )
+ .await?;
+ Ok(())
+ }
+}
+
+const OBJ_PATH: &str = "/lach/PolkitHelper";
+
+#[derive(Parser)]
+struct Opts {
+ /// Not recommended: start as a session connection, then use escalation
+ /// to respond to polkit requests.
+ #[arg(long)]
+ session: bool,
+}
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ tracing_subscriber::fmt::init();
+ let opts = Opts::parse();
+ let connection = if opts.session {
+ Connection::session().await
+ } else {
+ Connection::system().await
+ }
+ .context("failed to open connection")?;
+
+ let session = opts.session;
+ let blocking_connection: anyhow::Result<blocking::Connection> = spawn_blocking(move || {
+ Ok(if session {
+ blocking::Connection::session()?
+ } else {
+ blocking::Connection::system()?
+ })
+ })
+ .await?;
+ let blocking_connection = blocking_connection.context("failed to open blocking connection")?;
+
+ if opts.session {
+ setuid(Uid::from_raw(0))
+ .context("polkit-backend needs to be suid if run in session mode")?;
+ }
+
+ connection
+ .object_server()
+ .at(
+ OBJ_PATH,
+ Helper {
+ connection: connection.clone(),
+ blocking_connection,
+ },
+ )
+ .await
+ .context("failed listen path")?;
+
+ connection
+ .request_name("lach.polkit.helper1")
+ .await
+ .context("failed to request name")?;
+
+ pending().await
+}
remowt/cmds/remowt-agent/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-agent/Cargo.toml
@@ -0,0 +1,35 @@
+[package]
+name = "remowt-agent"
+description = "remowt on-host agent serving fs/pty/systemd endpoints over bifrostlink"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+bifrostlink.workspace = true
+bifrostlink-ports.workspace = true
+clap = { workspace = true, features = ["derive"] }
+futures.workspace = true
+nix.workspace = true
+remowt-polkit-shared.workspace = true
+remowt-link-shared.workspace = true
+remowt-plugin.workspace = true
+tempfile.workspace = true
+tokio = { workspace = true, features = [
+ "rt",
+ "fs",
+ "macros",
+ "net",
+ "io-util",
+ "time",
+ "process",
+] }
+tokio-util = { workspace = true, features = ["codec"] }
+tracing.workspace = true
+tracing-subscriber.workspace = true
+remowt-ui-prompt.workspace = true
+uuid = { workspace = true, features = ["v4"] }
+zbus = { workspace = true, features = ["tokio"] }
+zbus_polkit = { workspace = true, features = ["tokio"] }
+remowt-endpoints.workspace = true
remowt/cmds/remowt-agent/src/askpass.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-agent/src/askpass.rs
@@ -0,0 +1,51 @@
+use std::borrow::Cow;
+use std::io::Write as _;
+
+use anyhow::Context as _;
+use remowt_ui_prompt::dbus::{DbusPrompterInterface, DbusPrompterProxy, BUS_NAME, PROMPTER_PATH};
+use remowt_ui_prompt::{Prompter, Source};
+use tracing::debug;
+use zbus::Connection;
+
+pub async fn serve<P>(conn: &Connection, prompter: P) -> anyhow::Result<()>
+where
+ P: Prompter + 'static,
+{
+ conn.object_server()
+ .at(PROMPTER_PATH, DbusPrompterInterface(prompter))
+ .await?;
+ match conn.request_name(BUS_NAME).await {
+ Ok(()) => {}
+ Err(zbus::Error::NameTaken) => {
+ debug!("{BUS_NAME} already owned, chaining to upstream");
+ }
+ Err(e) => return Err(e.into()),
+ }
+ Ok(())
+}
+
+pub async fn ask(prompt: &str, description: String) -> anyhow::Result<()> {
+ let conn = Connection::session()
+ .await
+ .context("connecting to the session bus (DBUS_SESSION_BUS_ADDRESS)")?;
+ let proxy = DbusPrompterProxy::builder(&conn)
+ .destination(BUS_NAME)?
+ .path(PROMPTER_PATH)?
+ .build()
+ .await?;
+
+ let password = proxy
+ .prompt_text(
+ false,
+ prompt,
+ &description,
+ &[Source(Cow::Borrowed("remowt-askpass"))],
+ )
+ .await?;
+
+ let mut out = std::io::stdout().lock();
+ out.write_all(password.as_bytes())?;
+ out.write_all(b"\n")?;
+ out.flush()?;
+ Ok(())
+}
remowt/cmds/remowt-agent/src/bus.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-agent/src/bus.rs
@@ -0,0 +1,40 @@
+use std::process::Stdio;
+
+use anyhow::Context as _;
+use futures::StreamExt as _;
+use tokio::process::{Child, Command};
+use tokio_util::codec::{FramedRead, LinesCodec};
+use zbus::Connection;
+
+pub struct PrivateBus {
+ pub address: String,
+ pub conn: Connection,
+ _child: Child,
+}
+
+pub async fn spawn() -> anyhow::Result<PrivateBus> {
+ let mut child = Command::new("dbus-daemon")
+ .args(["--session", "--nofork", "--print-address"])
+ .stdout(Stdio::piped())
+ .kill_on_drop(true)
+ .spawn()
+ .context("spawning dbus-daemon for the private bus")?;
+
+ let stdout = child.stdout.take().expect("piped");
+ let address = FramedRead::new(stdout, LinesCodec::new())
+ .next()
+ .await
+ .context("dbus-daemon exited before printing its address")?
+ .context("reading dbus-daemon address")?;
+
+ let conn = zbus::connection::Builder::address(address.as_str())?
+ .build()
+ .await
+ .context("connecting to the private bus")?;
+
+ Ok(PrivateBus {
+ address,
+ conn,
+ _child: child,
+ })
+}
remowt/cmds/remowt-agent/src/editor.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-agent/src/editor.rs
@@ -0,0 +1,158 @@
+use std::env::{current_dir, temp_dir};
+use std::path::Path;
+use std::time::Duration;
+use std::{fs, io};
+
+use anyhow::{bail, Context as _};
+use nix::libc;
+use remowt_link_shared::editor::EditorEndpointsClient;
+use tokio::process::Command;
+use zbus::{fdo, interface, proxy, Connection};
+
+use remowt_link_shared::BifConfig;
+
+const BUS_NAME: &str = "lach.RemowtEditor";
+const SERVICE_PATH: &str = "/lach/Editor";
+
+pub struct EditorService {
+ editor: EditorEndpointsClient<BifConfig>,
+}
+
+#[interface(name = "lach.RemowtEditor")]
+impl EditorService {
+ /// Attach the User's GUI to the nvim server at `socket_path` (on the remote),
+ /// blocking until the user is done.
+ async fn edit(&self, socket_path: String) -> fdo::Result<()> {
+ self.editor
+ .open_editor(socket_path)
+ .await
+ .map_err(|e| fdo::Error::Failed(format!("requesting editor on the User: {e}")))?
+ .map_err(|e| fdo::Error::Failed(format!("editor failed: {e}")))?;
+ Ok(())
+ }
+
+ async fn forward_tcp(&self, addr: String) -> fdo::Result<u16> {
+ let local = self
+ .editor
+ .expose_tcp(addr)
+ .await
+ .map_err(|e| fdo::Error::Failed(format!("requesting tcp forward on the User: {e}")))?
+ .map_err(|e| fdo::Error::Failed(format!("tcp forward failed: {e}")))?;
+ Ok(local)
+ }
+
+ async fn forward_udp(&self, addr: String) -> fdo::Result<u16> {
+ let local = self
+ .editor
+ .expose_udp(addr)
+ .await
+ .map_err(|e| fdo::Error::Failed(format!("requesting udp forward on the User: {e}")))?
+ .map_err(|e| fdo::Error::Failed(format!("udp forward failed: {e}")))?;
+ Ok(local)
+ }
+}
+
+pub async fn serve(
+ conn: &Connection,
+ editor: EditorEndpointsClient<BifConfig>,
+) -> anyhow::Result<()> {
+ conn.object_server()
+ .at(SERVICE_PATH, EditorService { editor })
+ .await?;
+ conn.request_name(BUS_NAME).await?;
+ Ok(())
+}
+
+#[proxy(interface = "lach.RemowtEditor")]
+trait RemowtEditor {
+ async fn edit(&self, socket_path: &str) -> fdo::Result<()>;
+ async fn forward_tcp(&self, addr: &str) -> fdo::Result<u16>;
+ async fn forward_udp(&self, addr: &str) -> fdo::Result<u16>;
+}
+
+pub async fn forward(udp: bool, addr: String) -> anyhow::Result<()> {
+ let conn = Connection::session()
+ .await
+ .context("connecting to the session bus (DBUS_SESSION_BUS_ADDRESS)")?;
+ let proxy = RemowtEditorProxy::builder(&conn)
+ .destination(BUS_NAME)?
+ .path(SERVICE_PATH)?
+ .build()
+ .await?;
+ let local = if udp {
+ proxy.forward_udp(&addr).await?
+ } else {
+ proxy.forward_tcp(&addr).await?
+ };
+ println!("{local}");
+ Ok(())
+}
+
+pub async fn edit(path: String) -> anyhow::Result<()> {
+ let path = Path::new(&path);
+ let abs = if path.is_absolute() {
+ path.to_path_buf()
+ } else {
+ current_dir()?.join(path)
+ };
+
+ let sock = temp_dir().join(format!("remowt-nvim-{}.sock", uuid::Uuid::new_v4()));
+ let sock_str = sock
+ .to_str()
+ .context("temp socket path is not utf-8")?
+ .to_owned();
+
+ let mut child = Command::new("nvim");
+ child
+ .arg("--headless")
+ .arg("--listen")
+ .arg(&sock)
+ .arg("--")
+ .arg(&abs)
+ .kill_on_drop(true);
+ // SAFETY: only an async-signal-safe `prctl` call.
+ unsafe {
+ child.pre_exec(|| {
+ if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL as libc::c_ulong) != 0 {
+ return Err(io::Error::last_os_error());
+ }
+ Ok(())
+ });
+ }
+ let mut child = child.spawn().context("spawning nvim")?;
+
+ wait_for_socket(&sock)
+ .await
+ .context("nvim did not start its server")?;
+
+ let conn = Connection::session()
+ .await
+ .context("connecting to the session bus (DBUS_SESSION_BUS_ADDRESS)")?;
+ let proxy = RemowtEditorProxy::builder(&conn)
+ .destination(BUS_NAME)?
+ .path(SERVICE_PATH)?
+ .build()
+ .await?;
+ let result = proxy.edit(&sock_str).await;
+
+ if tokio::time::timeout(Duration::from_secs(2), child.wait())
+ .await
+ .is_err()
+ {
+ let _ = child.kill().await;
+ }
+ let _ = fs::remove_file(&sock);
+
+ result?;
+ Ok(())
+}
+
+async fn wait_for_socket(path: &Path) -> anyhow::Result<()> {
+ for _ in 0..200 {
+ if tokio::fs::try_exists(path).await.unwrap_or(false) {
+ return Ok(());
+ }
+ tokio::time::sleep(Duration::from_millis(50)).await;
+ }
+ bail!("timed out waiting for {}", path.display())
+}
remowt/cmds/remowt-agent/src/helper/dbus.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-agent/src/helper/dbus.rs
@@ -0,0 +1,81 @@
+use std::collections::HashMap;
+use std::marker::PhantomData;
+
+use remowt_polkit_shared::{BackendRequest, Identity};
+use remowt_ui_prompt::dbus::DbusPrompterInterface;
+use remowt_ui_prompt::Prompter;
+use zbus::Connection;
+
+use crate::PolkitHelperProxy;
+
+use super::Helper;
+
+struct TemporaryPrompterInterface<P: Prompter + 'static> {
+ connection: Connection,
+ path: String,
+ _marker: PhantomData<P>,
+}
+impl<P: Prompter + 'static> TemporaryPrompterInterface<P> {
+ async fn new(connection: Connection, prompter: P) -> Self {
+ let path = format!(
+ "/remowt/prompters/{}",
+ uuid::Uuid::new_v4().to_string().replace("-", "_")
+ );
+ let _ = connection
+ .object_server()
+ .at(path.clone(), DbusPrompterInterface(prompter))
+ .await;
+ Self {
+ connection,
+ path,
+ _marker: PhantomData,
+ }
+ }
+}
+impl<P: Prompter + Send + Sync + 'static> Drop for TemporaryPrompterInterface<P> {
+ fn drop(&mut self) {
+ // Removal is async because of async RwLock used inside...
+ // We should not care about its reuse
+ let connection = self.connection.clone();
+ let path = std::mem::take(&mut self.path);
+ tokio::spawn(async move {
+ let _ = connection
+ .object_server()
+ .remove::<DbusPrompterInterface<P>, String>(path)
+ .await;
+ });
+ }
+}
+
+#[derive(Clone)]
+pub struct DbusHelper {
+ connection: Connection,
+ helper: PolkitHelperProxy<'static>,
+}
+impl DbusHelper {
+ pub async fn new(connection: Connection) -> zbus::Result<Self> {
+ let helper = PolkitHelperProxy::new(&connection).await?;
+ Ok(Self { connection, helper })
+ }
+}
+impl Helper for DbusHelper {
+ async fn help_me<P: Prompter + Send + Sync + 'static>(
+ &self,
+ cookie: &str,
+ prompter: P,
+ identity: Identity,
+ ) -> anyhow::Result<()> {
+ let prompter = TemporaryPrompterInterface::new(self.connection.clone(), prompter).await;
+ self.helper
+ .init_conversation(
+ BackendRequest {
+ cookie: cookie.to_owned(),
+ environment: HashMap::new(),
+ prompter_path: prompter.path.clone(),
+ identity,
+ }, // cookie.to_owned(), HashMap::new(), prompter.path.clone()
+ )
+ .await?;
+ Ok(())
+ }
+}
remowt/cmds/remowt-agent/src/helper/mod.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-agent/src/helper/mod.rs
@@ -0,0 +1,21 @@
+use futures::Future;
+use remowt_polkit_shared::Identity;
+use remowt_ui_prompt::Prompter;
+
+mod dbus;
+mod protocol;
+mod socket;
+mod suid;
+
+pub use dbus::DbusHelper;
+pub use socket::SocketHelper;
+pub use suid::SuidHelper;
+
+pub trait Helper {
+ fn help_me<P: Prompter + Send + Sync + 'static>(
+ &self,
+ cookie: &str,
+ prompt: P,
+ identity: Identity,
+ ) -> impl Future<Output = anyhow::Result<()>> + Send;
+}
remowt/cmds/remowt-agent/src/helper/protocol.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-agent/src/helper/protocol.rs
@@ -0,0 +1,50 @@
+use std::pin::pin;
+
+use anyhow::bail;
+use futures::stream::Peekable;
+use futures::StreamExt as _;
+use remowt_ui_prompt::Prompter;
+use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt as _};
+use tokio::select;
+use tokio_util::codec::{FramedRead, LinesCodec};
+
+pub async fn run_conversation<R, W, P>(reader: R, mut writer: W, prompt: P) -> anyhow::Result<()>
+where
+ R: AsyncRead,
+ W: AsyncWrite + Unpin,
+ P: Prompter,
+{
+ let mut lines = pin!(FramedRead::new(reader, LinesCodec::new()).peekable());
+
+ while let Some(line) = lines.next().await {
+ let line = line?;
+ let res = if let Some(prompt_text) = line.strip_prefix("PAM_PROMPT_ECHO_OFF ") {
+ prompt.prompt_text(false, prompt_text, "", &[]).await?
+ } else if let Some(prompt_text) = line.strip_prefix("PAM_PROMPT_ECHO_ON ") {
+ prompt.prompt_text(true, prompt_text, "", &[]).await?
+ } else if let Some(msg_text) = line.strip_prefix("PAM_ERROR_MSG ") {
+ prompt.display_text(true, msg_text, &[]).await?;
+ String::new()
+ } else if let Some(msg_text) = line.strip_prefix("PAM_TEXT_INFO ") {
+ select! {
+ _ = Peekable::peek(lines.as_mut()) => {},
+ r = prompt.display_text(false, msg_text, &[]) => {r?}
+ }
+ String::new()
+ } else if line == "SUCCESS" {
+ return Ok(());
+ } else if line == "FAILURE" {
+ bail!("helper reported failure")
+ } else {
+ bail!("unknown agent request: {line}")
+ };
+
+ if res.contains('\n') {
+ bail!("response should not include newline")
+ }
+
+ writer.write_all(res.as_bytes()).await?;
+ writer.write_all(b"\n").await?;
+ }
+ bail!("agent finished unexpectedly")
+}
remowt/cmds/remowt-agent/src/helper/socket.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-agent/src/helper/socket.rs
@@ -0,0 +1,53 @@
+use anyhow::{anyhow, bail};
+use nix::unistd::User;
+use remowt_polkit_shared::Identity;
+use remowt_ui_prompt::Prompter;
+use tokio::io::AsyncWriteExt as _;
+use tokio::net::UnixStream;
+use tracing::debug;
+
+use super::protocol::run_conversation;
+use super::Helper;
+
+/// Polkit 127 introduced an alternative backend similar to `lach.PolkitHelper`
+const SOCKET_PATH: &str = "/run/polkit/agent-helper.socket";
+
+#[derive(Clone)]
+pub struct SocketHelper<F> {
+ pub fallback: F,
+}
+
+impl<F: Helper + Sync> Helper for SocketHelper<F> {
+ async fn help_me<P: Prompter + Send + Sync + 'static>(
+ &self,
+ cookie: &str,
+ prompt: P,
+ identity: Identity,
+ ) -> anyhow::Result<()> {
+ let Some(uid) = identity.uid() else {
+ bail!("can't process identity");
+ };
+
+ let stream = match UnixStream::connect(SOCKET_PATH).await {
+ Ok(stream) => stream,
+ Err(e) => {
+ debug!("agent-helper.socket unavailable ({e}), using fallback helper");
+ return self.fallback.help_me(cookie, prompt, identity).await;
+ }
+ };
+
+ let user = User::from_uid(uid)
+ .map_err(|e| anyhow!("error querying user: {e}"))?
+ .ok_or_else(|| anyhow!("user not found"))?;
+
+ assert!(!cookie.contains('\n'));
+ let (reader, mut writer) = stream.into_split();
+
+ writer.write_all(user.name.as_bytes()).await?;
+ writer.write_all(b"\n").await?;
+ writer.write_all(cookie.as_bytes()).await?;
+ writer.write_all(b"\n").await?;
+
+ run_conversation(reader, writer, prompt).await
+ }
+}
remowt/cmds/remowt-agent/src/helper/suid.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-agent/src/helper/suid.rs
@@ -0,0 +1,46 @@
+use std::process::Stdio;
+
+use anyhow::{anyhow, bail};
+use nix::unistd::User;
+use remowt_polkit_shared::Identity;
+use remowt_ui_prompt::Prompter;
+use tokio::io::AsyncWriteExt as _;
+use tokio::process::Command;
+
+use super::protocol::run_conversation;
+use super::Helper;
+
+#[derive(Clone)]
+pub struct SuidHelper;
+impl Helper for SuidHelper {
+ async fn help_me<P: Prompter + 'static>(
+ &self,
+ cookie: &str,
+ prompt: P,
+ identity: Identity,
+ ) -> anyhow::Result<()> {
+ let Some(uid) = identity.uid() else {
+ bail!("can't process identity");
+ };
+ let user = User::from_uid(uid)
+ .map_err(|e| anyhow!("error querying user: {e}"))?
+ .ok_or_else(|| anyhow!("user not found"))?;
+
+ let mut cmd = Command::new("polkit-agent-helper-1");
+ cmd.arg(user.name);
+ cmd.stdin(Stdio::piped());
+ cmd.stdout(Stdio::piped());
+ cmd.kill_on_drop(true);
+ let mut child = cmd.spawn()?;
+ let mut stdin = child.stdin.take().expect("piped");
+ let stdout = child.stdout.take().expect("piped");
+
+ assert!(!cookie.contains('\n'));
+ stdin.write_all(cookie.as_bytes()).await?;
+ stdin.write_all(b"\n").await?;
+
+ let res = run_conversation(stdout, stdin, prompt).await;
+ drop(child);
+ res
+ }
+}
remowt/cmds/remowt-agent/src/main.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-agent/src/main.rs
@@ -0,0 +1,437 @@
+use std::borrow::Cow;
+use std::collections::{BTreeMap, HashMap};
+use std::fs::Permissions;
+use std::future::pending;
+use std::io;
+use std::os::unix::fs::PermissionsExt as _;
+use std::path::PathBuf;
+use std::sync::{Arc, Mutex, OnceLock};
+
+use bifrostlink::declarative::RemoteEndpoints;
+use bifrostlink::Rpc;
+use bifrostlink_ports::stdio::from_stdio;
+use bifrostlink_ports::unix_socket::from_socket;
+use clap::Parser;
+use remowt_endpoints::{
+ forward::Forward, fs::Fs, iroh_tunnel::IrohTunnel, nix_daemon::NixDaemon, pty::Pty,
+ subprocess::Subprocess, systemd::Systemd,
+};
+use remowt_link_shared::iroh_tunnel::TunnelDialer;
+use remowt_link_shared::{editor::EditorEndpointsClient, Address, BifConfig};
+use remowt_polkit_shared::{emphasize, BackendRequest, Identity, PidDisplay};
+use remowt_ui_prompt::bifrost::PromptEndpointsClient;
+use remowt_ui_prompt::rofi::RofiPrompter;
+use remowt_ui_prompt::{PrependSourcePrompter, Prompter, Source};
+use tokio::fs;
+use tokio::net::UnixStream;
+use tokio::runtime::Builder;
+use tokio::task::AbortHandle;
+use tracing::{debug, trace};
+use zbus::fdo;
+use zbus::zvariant::{OwnedValue, Str};
+use zbus::{interface, proxy, Connection};
+use zbus_polkit::policykit1::Subject;
+
+use self::helper::{Helper, SocketHelper, SuidHelper};
+
+pub mod askpass;
+pub mod bus;
+pub mod editor;
+pub mod helper;
+
+struct CancelTaskOnDrop {
+ tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,
+ handle: String,
+}
+impl Drop for CancelTaskOnDrop {
+ fn drop(&mut self) {
+ debug!("cancel on drop");
+ if let Some(task) = self
+ .tasks
+ .lock()
+ .expect("not poisoned")
+ .remove(&self.handle)
+ {
+ task.abort();
+ }
+ }
+}
+
+struct Agent<H, P> {
+ tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,
+ helper: H,
+ prompter: P,
+}
+impl<H, P> Agent<H, P> {
+ fn new(helper: H, prompter: P) -> Self {
+ Agent {
+ tasks: Arc::new(Mutex::new(HashMap::new())),
+ helper,
+ prompter,
+ }
+ }
+}
+
+#[interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]
+impl<H, P> Agent<H, P>
+where
+ H: Helper + Clone + Send + Sync + 'static,
+ P: Prompter + Clone + Send + Sync + 'static,
+{
+ /// BeginAuthentication method
+ #[allow(clippy::too_many_arguments)]
+ async fn begin_authentication(
+ &self,
+ action_id: String,
+ message: String,
+ _icon_name: String,
+ mut details: BTreeMap<String, String>,
+ cookie: String,
+ identities: Vec<Identity>,
+ ) -> zbus::fdo::Result<()> {
+ use std::fmt::Write;
+ debug!("begin auth");
+ let _cancel_guard = Arc::new(OnceLock::new());
+ let task = {
+ let helper = self.helper.clone();
+ let prompter = self.prompter.clone();
+ let cookie = cookie.clone();
+ let _cancel_guard = _cancel_guard.clone();
+ tokio::task::spawn(async move {
+ let _cancel_guard = _cancel_guard.clone();
+ trace!("conversation task");
+ let mut description = format!("{message}\n\n<b>Action id:</b> {action_id}",);
+ if let Some(subject) = details.remove("polkit.caller-pid") {
+ let _ = write!(description, "\n<b>Caller:</b> ");
+ if let Ok(pid) = subject.parse::<u32>() {
+ let _ = write!(description, "{}", PidDisplay(pid));
+ } else {
+ let _ = write!(description, "{}", emphasize("invalid pid"));
+ }
+ }
+ if let Some(subject) = details.remove("polkit.subject-pid") {
+ let _ = write!(description, "\n<b>Subject:</b> ");
+ if let Ok(pid) = subject.parse::<u32>() {
+ let _ = write!(description, "{}", PidDisplay(pid));
+ } else {
+ let _ = write!(description, "{}", emphasize("invalid pid"));
+ }
+ }
+ let mut prompter = PrependSourcePrompter {
+ source: vec![Source(Cow::Borrowed("polkit agent"))],
+ description: description.clone(),
+ prompter,
+ };
+
+ let identity_displays: Vec<String> =
+ identities.iter().map(|v| v.to_string()).collect();
+ let identity_displays: Vec<&str> =
+ identity_displays.iter().map(|v| v.as_str()).collect();
+ debug!("choose identity");
+ let choosen_identity = match identity_displays.len() {
+ 0 => {
+ return Err(fdo::Error::AuthFailed(
+ "no identity to authenticate as".to_owned(),
+ ))
+ }
+ 1 => 0,
+ _ => {
+ prompter
+ .prompt_enum(
+ "Identity",
+ "Select identity to use for polkit authorization",
+ &identity_displays,
+ &[],
+ )
+ .await?
+ }
+ };
+ debug!("identity chosen");
+
+ let _ = write!(
+ description,
+ "\n<b>Identity:</b> {}",
+ identities[choosen_identity as usize]
+ );
+ prompter.description = description;
+
+ prompter.source.push(Source(Cow::Borrowed("polkit daemon")));
+
+ helper
+ .help_me(
+ &cookie,
+ prompter,
+ identities[choosen_identity as usize].clone(),
+ )
+ .await
+ .map_err(|e| fdo::Error::Failed(e.to_string()))?;
+ // let connection = Connection::system().await?;
+ // let helper = PolkitHelperProxy::new(&connection).await?;
+
+ Ok(())
+ })
+ };
+ self.tasks
+ .lock()
+ .unwrap()
+ .insert(cookie.clone(), task.abort_handle());
+ debug!("abort handle stored");
+ let _ = _cancel_guard.set(CancelTaskOnDrop {
+ tasks: self.tasks.clone(),
+ handle: cookie.clone(),
+ });
+
+ let _ = task.await;
+
+ Ok(())
+ }
+
+ /// CancelAuthentication method
+ async fn cancel_authentication(&self, cookie: &str) -> zbus::fdo::Result<()> {
+ debug!("auth cancelled");
+ if let Some(abort) = self.tasks.lock().unwrap().remove(cookie) {
+ debug!("abort handle found");
+ abort.abort();
+ }
+ // debug!("Authentication cancled ! {cookie}");
+ Ok(())
+ }
+}
+
+const OBJ_PATH: &str = "/org/freedesktop/PolicyKit1/AuthenticationAgent";
+
+#[proxy(
+ interface = "lach.PolkitHelper",
+ default_service = "lach.polkit.helper1",
+ default_path = "/lach/PolkitHelper"
+)]
+trait PolkitHelper {
+ fn init_conversation(&self, request: BackendRequest) -> zbus::Result<()>;
+}
+
+#[derive(Parser)]
+enum Opts {
+ AskPass {
+ prompt: String,
+ description: String,
+ },
+ Editor {
+ /// Argument to nvim
+ path: String,
+ },
+ /// Expose a remote TCP/UDP address on the local (User) machine, printing the local port.
+ Forward {
+ /// Protocol: `tcp` (ssh or iroh) or `udp` (iroh-only).
+ proto: ForwardProto,
+ /// Remote socket address to expose locally, e.g. `127.0.0.1:8080`.
+ addr: String,
+ },
+ RealAgent {
+ #[arg(long)]
+ path: Option<PathBuf>,
+ /// Expect own address to be AgentPrivileged, skip installing polkit agent
+ #[arg(long)]
+ privileged: bool,
+ #[arg(long)]
+ local: bool,
+ },
+ LocalAgent,
+}
+
+#[derive(Clone, Copy, clap::ValueEnum)]
+enum ForwardProto {
+ Tcp,
+ Udp,
+}
+
+fn main() -> anyhow::Result<()> {
+ tracing_subscriber::fmt()
+ .with_writer(io::stderr)
+ .without_time()
+ .init();
+ let opts = Opts::parse();
+
+ let runtime = Builder::new_current_thread().enable_all().build()?;
+
+ match opts {
+ Opts::AskPass {
+ prompt,
+ description,
+ } => runtime.block_on(askpass::ask(&prompt, description)),
+ Opts::LocalAgent => runtime.block_on(main_real()),
+ Opts::Editor { path } => runtime.block_on(editor::edit(path)),
+ Opts::Forward { proto, addr } => {
+ runtime.block_on(editor::forward(matches!(proto, ForwardProto::Udp), addr))
+ }
+ Opts::RealAgent {
+ path,
+ privileged,
+ local,
+ } => runtime.block_on(main_real_agent(path, privileged, local)),
+ }
+}
+async fn main_real() -> anyhow::Result<()> {
+ let system_conn = Connection::system().await?;
+ let helper = SocketHelper {
+ fallback: SuidHelper,
+ };
+ register_auth_agent(&system_conn, Agent::new(helper, RofiPrompter)).await?;
+
+ let session_conn = Connection::session().await?;
+ askpass::serve(&session_conn, RofiPrompter).await?;
+
+ let _keep_alive = (system_conn, session_conn);
+ pending().await
+}
+async fn main_real_agent(
+ path: Option<PathBuf>,
+ privileged: bool,
+ local: bool,
+) -> anyhow::Result<()> {
+ let address = if privileged {
+ Address::AgentPrivileged
+ } else {
+ Address::Agent
+ };
+ let mut rpc = Rpc::<BifConfig>::new(address);
+
+ let dialer = Arc::new(TunnelDialer::new());
+ Fs::new().register_endpoints(&mut rpc);
+ Systemd.register_endpoints(&mut rpc);
+ Pty::new(dialer.clone()).register_endpoints(&mut rpc);
+ Subprocess::new(dialer.clone()).register_endpoints(&mut rpc);
+ NixDaemon::new(dialer.clone()).register_endpoints(&mut rpc);
+ IrohTunnel::new(dialer.clone()).register_endpoints(&mut rpc);
+ Forward::new(dialer.clone()).register_endpoints(&mut rpc);
+
+ remowt_plugin::host::serve(&mut rpc);
+
+ let user_prompter = PromptEndpointsClient::wrap(rpc.remote(Address::User));
+ let editor_client = EditorEndpointsClient::wrap(rpc.remote(Address::User));
+
+ let bus = bus::spawn().await?;
+ askpass::serve(&bus.conn, user_prompter.clone()).await?;
+ editor::serve(&bus.conn, editor_client).await?;
+
+ let helpers = tempfile::Builder::new().prefix("remowt-path.").tempdir()?;
+ let exe = std::env::current_exe()?;
+ let askpass_helper = helpers.path().join("remowt-askpass");
+ let editor_helper = helpers.path().join("remowt-editor");
+ let forward_helper = helpers.path().join("remowt-forward");
+ {
+ let script = format!(
+ "#!/bin/sh\nexec {} ask-pass \"password\" \"$1\"\n",
+ sh_quote(&exe.to_string_lossy())
+ );
+ fs::write(&askpass_helper, script).await?;
+ fs::set_permissions(&askpass_helper, Permissions::from_mode(0o755)).await?;
+ }
+ {
+ let script = format!(
+ "#!/bin/sh\nexec {} editor \"$1\"\n",
+ sh_quote(&exe.to_string_lossy())
+ );
+ fs::write(&editor_helper, script).await?;
+ fs::set_permissions(&editor_helper, Permissions::from_mode(0o755)).await?;
+ }
+ {
+ let script = format!(
+ "#!/bin/sh\nexec {} forward \"$@\"\n",
+ sh_quote(&exe.to_string_lossy())
+ );
+ fs::write(&forward_helper, script).await?;
+ fs::set_permissions(&forward_helper, Permissions::from_mode(0o755)).await?;
+ }
+
+ // Safety: Hoping tokio own threads won't read any of those...
+ unsafe {
+ prepend_path(helpers.path());
+ std::env::set_var("SUDO_ASKPASS", &askpass_helper);
+ std::env::set_var("SSH_ASKPASS", &askpass_helper);
+ std::env::set_var("SSH_ASKPASS_REQUIRE", "force");
+ std::env::set_var("EDITOR", &editor_helper);
+ std::env::set_var("VISUAL", &editor_helper);
+ std::env::set_var("DBUS_SESSION_BUS_ADDRESS", &bus.address);
+ }
+
+ let port = match path {
+ Some(path) => from_socket(UnixStream::connect(path).await?),
+ None => from_stdio(),
+ };
+ rpc.add_direct(Address::User, port, bifrostlink::Rtt(0));
+
+ let polkit_conn = if !privileged && !local {
+ // The unprivileged agent doubles as a polkit authentication agent so
+ // `run0` (e.g. our own elevation) routes its prompt to the User over
+ // bifrost instead of failing on a tty-less session.
+ let conn = Connection::system().await?;
+ let helper = SocketHelper {
+ fallback: SuidHelper,
+ };
+ register_auth_agent(&conn, Agent::new(helper, user_prompter)).await?;
+ Some(conn)
+ } else {
+ None
+ };
+
+ let _keep_alive = (bus, helpers, polkit_conn);
+ pending().await
+}
+
+async fn register_auth_agent<H, P>(conn: &Connection, agent: Agent<H, P>) -> anyhow::Result<()>
+where
+ H: Helper + Clone + Send + Sync + 'static,
+ P: Prompter + Clone + Send + Sync + 'static,
+{
+ let proxy = zbus_polkit::policykit1::AuthorityProxy::new(conn).await?;
+ conn.object_server().at(OBJ_PATH, agent).await?;
+
+ let subject = auth_agent_subject()?;
+ proxy
+ .register_authentication_agent(&subject, "C", OBJ_PATH)
+ .await?;
+ debug!(kind = subject.subject_kind, "registered polkit agent");
+ Ok(())
+}
+
+fn auth_agent_subject() -> anyhow::Result<Subject> {
+ let mut details = HashMap::new();
+ if let Ok(session_id) = std::env::var("XDG_SESSION_ID") {
+ let val: OwnedValue = Str::from(session_id).into();
+ details.insert("session-id".to_string(), val);
+ return Ok(Subject {
+ subject_kind: "unix-session".to_string(),
+ subject_details: details,
+ });
+ }
+
+ details.insert("pid".to_string(), OwnedValue::from(std::process::id()));
+ Ok(Subject {
+ subject_kind: "unix-process".to_string(),
+ subject_details: details,
+ })
+}
+
+fn sh_quote(s: &str) -> String {
+ format!("'{}'", s.replace('\'', "'\\''"))
+}
+
+/// Prepend `dir` to the process `PATH`.
+///
+/// # SAFETY
+///
+/// Same as `set_var`
+unsafe fn prepend_path(dir: &std::path::Path) {
+ let value = match std::env::var_os("PATH") {
+ Some(existing) => {
+ let mut v = dir.as_os_str().to_owned();
+ v.push(":");
+ v.push(existing);
+ v
+ }
+ None => dir.as_os_str().to_owned(),
+ };
+ unsafe {
+ std::env::set_var("PATH", value);
+ }
+}
remowt/cmds/remowt-ssh/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-ssh/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "remowt-ssh"
+description = "SSH transport client for connecting to a remowt agent"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+clap = { workspace = true, features = ["derive"] }
+tracing-subscriber.workspace = true
+remowt-link-shared.workspace = true
+remowt-client.workspace = true
+tokio = { workspace = true, features = [
+ "macros",
+ "fs",
+ "net",
+ "io-util",
+ "rt",
+ "signal",
+] }
+nix = { workspace = true, features = ["term"] }
+anyhow.workspace = true
+tracing.workspace = true
+remowt-ui-prompt.workspace = true
remowt/cmds/remowt-ssh/src/main.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-ssh/src/main.rs
@@ -0,0 +1,230 @@
+use std::borrow::Cow;
+use std::env::VarError;
+use std::io;
+use std::os::fd::{AsRawFd, RawFd};
+use std::path::PathBuf;
+use std::pin::Pin;
+use std::task::{Context, Poll};
+
+use anyhow::anyhow;
+use clap::Parser;
+use nix::libc;
+use nix::sys::termios::{self, SetArg, Termios};
+use remowt_client::editor::SshEditor;
+use remowt_client::{AgentBundle, Remowt};
+use remowt_link_shared::editor::serve_editor;
+use remowt_ui_prompt::auto::AutoPrompter;
+use remowt_ui_prompt::bifrost::serve_prompts;
+use remowt_ui_prompt::{PrependSourcePrompter, Source};
+use tokio::io::unix::AsyncFd;
+use tokio::io::{AsyncRead, ReadBuf};
+use tokio::signal::unix::{signal, SignalKind};
+use tracing::debug;
+
+#[derive(Parser)]
+enum Opts {
+ /// Connect to remote host with remowt agent.
+ Ssh {
+ host: String,
+ #[arg(long)]
+ escalate: bool,
+ },
+ /// Connect to local host for testing the connectivity.
+ Local {
+ #[arg(long)]
+ escalate: bool,
+ },
+}
+
+fn agents_dir() -> anyhow::Result<PathBuf> {
+ std::env::var_os("REMOWT_AGENTS_DIR")
+ .map(PathBuf::from)
+ .or_else(|| option_env!("REMOWT_AGENTS_DIR").map(PathBuf::from))
+ .ok_or_else(|| anyhow!("no remowt-agents bundle"))
+}
+
+#[tokio::main(flavor = "current_thread")]
+async fn main() -> anyhow::Result<()> {
+ tracing_subscriber::fmt()
+ .with_writer(std::io::stderr)
+ .without_time()
+ .init();
+ let opts = Opts::parse();
+
+ let bundle = AgentBundle::from_dir(agents_dir()?)?;
+ let (conn, escalate) = match &opts {
+ Opts::Ssh { host, escalate } => (
+ Remowt::connect(host, &bundle, "remowt-ssh".to_owned()).await?,
+ *escalate,
+ ),
+ Opts::Local { escalate } => (
+ Remowt::connect_local(&bundle, "remowt-ssh".to_owned()).await?,
+ *escalate,
+ ),
+ };
+ let mut rpc = conn.rpc();
+
+ serve_prompts(
+ &mut rpc,
+ PrependSourcePrompter {
+ prompter: AutoPrompter::new().await,
+ source: match opts {
+ Opts::Ssh { host, .. } => vec![Source(Cow::Owned(format!("ssh host: {}", host)))],
+ Opts::Local { .. } => vec![],
+ },
+ description: "".to_owned(),
+ },
+ );
+ if let Some(sess) = conn.ssh() {
+ serve_editor(
+ &mut rpc,
+ SshEditor {
+ sess,
+ conn: conn.clone(),
+ },
+ );
+ }
+
+ debug!("entering shell");
+ run_shell(&conn, escalate).await?;
+ debug!("shell ended");
+
+ Ok(())
+}
+
+async fn run_shell(conn: &Remowt, escalate: bool) -> anyhow::Result<()> {
+ let term = match std::env::var("TERM") {
+ Ok(v) => v,
+ Err(VarError::NotPresent) => "xterm-256color".to_owned(),
+ Err(e) => return Err(e.into()),
+ };
+ let (cols, rows) = term_size().unwrap_or((80, 24));
+
+ let shell = conn.open_shell(&term, cols, rows, escalate).await?;
+ let resizer = shell.resizer();
+ let stream = shell.stream;
+
+ let _raw = RawMode::enable();
+
+ if let Ok(mut winch) = signal(SignalKind::window_change()) {
+ tokio::spawn(async move {
+ while winch.recv().await.is_some() {
+ if let Some((cols, rows)) = term_size() {
+ let _ = resizer.resize(cols, rows).await;
+ }
+ }
+ });
+ }
+
+ let (mut from_remote, mut to_remote) = tokio::io::split(stream);
+ let mut stdin = AsyncStdin::new()?;
+ let mut stdout = tokio::io::stdout();
+
+ tokio::select! {
+ r = tokio::io::copy(&mut from_remote, &mut stdout) => { r?; }
+ _ = tokio::io::copy(&mut stdin, &mut to_remote) => {}
+ }
+
+ Ok(())
+}
+
+struct AsyncStdin {
+ fd: AsyncFd<RawFd>,
+ original_flags: i32,
+}
+
+impl AsyncStdin {
+ fn new() -> io::Result<Self> {
+ let raw = libc::STDIN_FILENO;
+ // SAFETY: F_GETFL/F_SETFL round-trip on a valid fd.
+ let original_flags = unsafe { libc::fcntl(raw, libc::F_GETFL) };
+ if original_flags < 0 {
+ return Err(io::Error::last_os_error());
+ }
+ if unsafe { libc::fcntl(raw, libc::F_SETFL, original_flags | libc::O_NONBLOCK) } < 0 {
+ return Err(io::Error::last_os_error());
+ }
+ Ok(Self {
+ fd: AsyncFd::new(raw)?,
+ original_flags,
+ })
+ }
+}
+
+impl Drop for AsyncStdin {
+ fn drop(&mut self) {
+ // SAFETY: restoring the flags we saved on a valid fd.
+ unsafe { libc::fcntl(libc::STDIN_FILENO, libc::F_SETFL, self.original_flags) };
+ }
+}
+
+impl AsyncRead for AsyncStdin {
+ fn poll_read(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &mut ReadBuf<'_>,
+ ) -> Poll<io::Result<()>> {
+ let this = self.get_mut();
+ loop {
+ let mut guard = match this.fd.poll_read_ready(cx) {
+ Poll::Ready(Ok(g)) => g,
+ Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
+ Poll::Pending => return Poll::Pending,
+ };
+ let unfilled = buf.initialize_unfilled();
+ let res = guard.try_io(|inner| {
+ let fd = *inner.get_ref();
+ // SAFETY: writing into `unfilled`'s own backing storage.
+ let n = unsafe { libc::read(fd, unfilled.as_mut_ptr().cast(), unfilled.len()) };
+ if n < 0 {
+ Err(io::Error::last_os_error())
+ } else {
+ Ok(n as usize)
+ }
+ });
+ match res {
+ Ok(Ok(n)) => {
+ buf.advance(n);
+ return Poll::Ready(Ok(()));
+ }
+ Ok(Err(e)) => return Poll::Ready(Err(e)),
+ Err(_would_block) => continue,
+ }
+ }
+ }
+}
+
+fn term_size() -> Option<(u16, u16)> {
+ let mut ws: libc::winsize = unsafe { std::mem::zeroed() };
+ let rc = unsafe { libc::ioctl(libc::STDIN_FILENO, libc::TIOCGWINSZ, &mut ws) };
+ if rc != 0 || ws.ws_col == 0 {
+ None
+ } else {
+ Some((ws.ws_col, ws.ws_row))
+ }
+}
+
+struct RawMode {
+ original: Termios,
+}
+
+impl RawMode {
+ fn enable() -> Option<Self> {
+ let stdin = std::io::stdin();
+ // SAFETY: trivial libc call on a borrowed fd.
+ if unsafe { libc::isatty(stdin.as_raw_fd()) } != 1 {
+ return None;
+ }
+ let original = termios::tcgetattr(&stdin).ok()?;
+ let mut raw = original.clone();
+ termios::cfmakeraw(&mut raw);
+ termios::tcsetattr(&stdin, SetArg::TCSANOW, &raw).ok()?;
+ Some(Self { original })
+ }
+}
+
+impl Drop for RawMode {
+ fn drop(&mut self) {
+ let _ = termios::tcsetattr(std::io::stdin(), SetArg::TCSANOW, &self.original);
+ }
+}
remowt/crates/polkit-shared/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/polkit-shared/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "remowt-polkit-shared"
+description = "Shared polkit/PAM types for remowt"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+nix = { workspace = true, features = ["user"] }
+serde = { workspace = true, features = ["derive"] }
+zbus.workspace = true
remowt/crates/polkit-shared/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/polkit-shared/src/lib.rs
@@ -0,0 +1,113 @@
+use std::collections::HashMap;
+use std::{fmt, fs};
+
+use nix::unistd::{Uid, User};
+use serde::{Deserialize, Serialize};
+use zbus::zvariant::{OwnedValue, Type, Value};
+
+pub fn emphasize(s: impl AsRef<str>) -> String {
+ format!("<span style=\"italic\"><{}></span>", escape(s),)
+}
+fn command(s: impl AsRef<str>) -> String {
+ format!("<u><tt>{}</tt></u>", s.as_ref())
+}
+fn escape(s: impl AsRef<str>) -> String {
+ s.as_ref()
+ .replace("&", """)
+ .replace("<", "<")
+ .replace(">", ">")
+}
+
+pub struct PidDisplay(pub u32);
+impl fmt::Display for PidDisplay {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if self.0 == 1 {
+ emphasize("init").fmt(f)
+ } else if let Ok(proc) = fs::read_to_string(format!("/proc/{}/cmdline", self.0)) {
+ write!(
+ f,
+ "command: {}",
+ command(
+ proc.replace("\0", " ")
+ .strip_suffix(" ")
+ .expect("cmdline should end with NUL")
+ )
+ )
+ } else if let Ok(proc) = fs::read_to_string(format!("/proc/{}/comm", self.0)) {
+ write!(f, "process: {}", command(proc.replace("\0", " ")))
+ } else {
+ emphasize("unknown process").fmt(f)
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize, Type, PartialEq, Debug)]
+pub struct Identity {
+ pub kind: String,
+ pub details: HashMap<String, OwnedValue>,
+}
+
+impl Identity {
+ pub fn uid(&self) -> Option<Uid> {
+ if self.kind != "unix-user" {
+ return None;
+ }
+ let uid = self.details.get("uid")?;
+ let Value::U32(uid) = &**uid else {
+ return None;
+ };
+ Some(Uid::from_raw(*uid))
+ }
+}
+
+impl fmt::Display for Identity {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self.kind.as_str() {
+ "unix-user" => match self.details.get("uid").map(|v| &**v) {
+ Some(Value::U32(uid)) => match User::from_uid(Uid::from_raw(*uid)) {
+ Ok(Some(u)) => write!(
+ f,
+ "user: {} {} {}",
+ u.name,
+ u.uid,
+ if u.gecos.is_empty() {
+ "".to_owned()
+ } else {
+ format!(": {}", escape(u.gecos.to_string_lossy()))
+ }
+ ),
+ Ok(None) => emphasize("not found").fmt(f),
+ Err(e) => {
+ let user = format!("could not get user: {e}");
+ emphasize(&user).fmt(f)?;
+ Ok(())
+ }
+ },
+
+ _ => emphasize("unknown uid").fmt(f),
+ },
+ _ => emphasize(format!("identity of unknown kind: {}", self.kind)).fmt(f),
+ }
+ }
+}
+
+impl Clone for Identity {
+ fn clone(&self) -> Self {
+ Self {
+ kind: self.kind.clone(),
+ details: self
+ .details
+ .iter()
+ .map(|(k, v)| (k.clone(), v.try_clone().expect("no fds are expected")))
+ .collect(),
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize, Type, PartialEq, Debug)]
+pub struct BackendRequest {
+ pub cookie: String,
+ pub environment: HashMap<String, String>,
+ pub prompter_path: String,
+ pub identity: Identity,
+}
remowt/crates/remowt-client/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-client/Cargo.toml
@@ -0,0 +1,32 @@
+[package]
+name = "remowt-client"
+description = "russh-based client connection to a remowt agent"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+bifrostlink.workspace = true
+bytes.workspace = true
+camino.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+remowt-link-shared.workspace = true
+russh.workspace = true
+russh-config.workspace = true
+tempfile.workspace = true
+tokio = { workspace = true, features = [
+ "net",
+ "io-util",
+ "rt",
+ "sync",
+ "macros",
+ "process",
+] }
+tracing.workspace = true
+uuid = { workspace = true, features = ["v4"] }
+remowt-endpoints.workspace = true
+tokio-util = { workspace = true, features = ["codec"] }
+futures.workspace = true
+iroh.workspace = true
remowt/crates/remowt-client/src/editor.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-client/src/editor.rs
@@ -0,0 +1,183 @@
+use std::collections::HashMap;
+use std::net::SocketAddr;
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::{Arc, Mutex};
+
+use remowt_endpoints::forward::ForwardClient;
+use remowt_link_shared::editor::{EditorBackend, Error};
+use remowt_link_shared::BifConfig;
+use russh::client::Handle;
+use tokio::net::{TcpListener, UdpSocket, UnixListener};
+use tracing::error;
+
+use crate::{Remowt, SshHandler};
+
+pub struct SshEditor {
+ pub sess: Arc<Handle<SshHandler>>,
+ pub conn: Remowt,
+}
+impl EditorBackend for SshEditor {
+ async fn open_editor(&self, socket_path: String) -> Result<(), Error> {
+ let local = std::env::temp_dir().join(format!("remowt-nvim-{}.sock", uuid::Uuid::new_v4()));
+ let _ = std::fs::remove_file(&local);
+ let listener = UnixListener::bind(&local).map_err(|e| Error::Failed(e.to_string()))?;
+
+ let sess = self.sess.clone();
+ let forward = tokio::spawn(async move {
+ loop {
+ let Ok((mut stream, _)) = listener.accept().await else {
+ break;
+ };
+ let sess = sess.clone();
+ let remote = socket_path.clone();
+ tokio::spawn(async move {
+ match sess.channel_open_direct_streamlocal(remote).await {
+ Ok(ch) => {
+ let mut remote = ch.into_stream();
+ let _ = tokio::io::copy_bidirectional(&mut stream, &mut remote).await;
+ }
+ Err(e) => error!("opening direct-streamlocal to nvim failed: {e}"),
+ }
+ });
+ }
+ });
+
+ let status = tokio::process::Command::new("neovide")
+ .arg("--no-fork")
+ .arg("--server")
+ .arg(&local)
+ .status()
+ .await
+ .map_err(|e| Error::Failed(format!("spawning neovide: {e}")));
+
+ forward.abort();
+ let _ = std::fs::remove_file(&local);
+
+ match status? {
+ s if s.success() => Ok(()),
+ s => Err(Error::Failed(format!("neovide exited with {s}"))),
+ }
+ }
+
+ async fn expose_tcp(&self, addr: String) -> Result<u16, Error> {
+ let listener = TcpListener::bind(("127.0.0.1", 0))
+ .await
+ .map_err(|e| Error::Failed(e.to_string()))?;
+ let local = listener
+ .local_addr()
+ .map_err(|e| Error::Failed(e.to_string()))?
+ .port();
+
+ let conn = self.conn.clone();
+ tokio::spawn(async move {
+ loop {
+ let Ok((mut tcp, _)) = listener.accept().await else {
+ break;
+ };
+ let conn = conn.clone();
+ let addr = addr.clone();
+ tokio::spawn(async move {
+ let (forwarded, tunnel) = match conn.bind_fast_tunnel("forward", false).await {
+ Ok(v) => v,
+ Err(e) => {
+ error!("forward: bind tunnel failed: {e}");
+ return;
+ }
+ };
+ let fclient: ForwardClient<BifConfig> = conn.endpoints();
+ match fclient.connect_tcp(tunnel, addr).await {
+ Ok(Ok(())) => {}
+ Ok(Err(e)) => {
+ error!("forward: agent connect_tcp failed: {e}");
+ return;
+ }
+ Err(e) => {
+ error!("forward: connect_tcp rpc failed: {e}");
+ return;
+ }
+ }
+ match forwarded.accept().await {
+ Ok(mut stream) => {
+ let _ = tokio::io::copy_bidirectional(&mut tcp, &mut stream).await;
+ }
+ Err(e) => error!("forward: accept tunnel failed: {e}"),
+ }
+ });
+ }
+ });
+
+ Ok(local)
+ }
+
+ async fn expose_udp(&self, addr: String) -> Result<u16, Error> {
+ let router = self.conn.datagram_router().ok_or_else(|| {
+ Error::Failed(
+ "udp forward requires the iroh fast path, which is not established".into(),
+ )
+ })?;
+
+ let fclient: ForwardClient<BifConfig> = self.conn.endpoints();
+ let session = fclient
+ .open_udp(addr)
+ .await
+ .map_err(|e| Error::Failed(format!("open_udp rpc: {e}")))?
+ .map_err(|e| Error::Failed(format!("agent open_udp: {e}")))?;
+
+ let sock = Arc::new(
+ UdpSocket::bind(("127.0.0.1", 0))
+ .await
+ .map_err(|e| Error::Failed(e.to_string()))?,
+ );
+ let local = sock
+ .local_addr()
+ .map_err(|e| Error::Failed(e.to_string()))?
+ .port();
+
+ let sub_for_source: Arc<Mutex<HashMap<SocketAddr, u64>>> =
+ Arc::new(Mutex::new(HashMap::new()));
+ let source_for_sub: Arc<Mutex<HashMap<u64, SocketAddr>>> =
+ Arc::new(Mutex::new(HashMap::new()));
+ let next_sub = Arc::new(AtomicU64::new(0));
+ let mut rx = router.register(session);
+
+ let up_sock = sock.clone();
+ let up_router = router.clone();
+ let down_source_for_sub = source_for_sub.clone();
+ tokio::spawn(async move {
+ let mut buf = vec![0u8; 65535];
+ loop {
+ let (n, src) = match up_sock.recv_from(&mut buf).await {
+ Ok(v) => v,
+ Err(_) => break,
+ };
+ let sub = {
+ let mut by_src = sub_for_source.lock().expect("lock");
+ if let Some(&sub) = by_src.get(&src) {
+ sub
+ } else {
+ let sub = next_sub.fetch_add(1, Ordering::Relaxed);
+ by_src.insert(src, sub);
+ source_for_sub.lock().expect("lock").insert(sub, src);
+ sub
+ }
+ };
+ if up_router.send(session, sub, &buf[..n]).is_err() {
+ break;
+ }
+ }
+ up_router.unregister(session);
+ });
+
+ let down_sock = sock.clone();
+ tokio::spawn(async move {
+ while let Some((sub, payload)) = rx.recv().await {
+ let dst = down_source_for_sub.lock().expect("lock").get(&sub).copied();
+ if let Some(dst) = dst {
+ let _ = down_sock.send_to(&payload, dst).await;
+ }
+ }
+ });
+
+ Ok(local)
+ }
+}
remowt/crates/remowt-client/src/forwarded.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-client/src/forwarded.rs
@@ -0,0 +1,89 @@
+use std::io;
+use std::pin::Pin;
+use std::task::{Context, Poll};
+
+use anyhow::{anyhow, Result};
+use camino::Utf8PathBuf;
+use remowt_link_shared::iroh_tunnel::IrohBiStream;
+use russh::client::Msg;
+use russh::{Channel, ChannelStream};
+use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
+use tokio::net::{UnixListener, UnixStream};
+use tokio::sync::oneshot;
+
+pub enum RemowtListener {
+ Ssh(oneshot::Receiver<Channel<Msg>>),
+ Local(UnixListener, Utf8PathBuf),
+ Iroh(oneshot::Receiver<RemowtStream>),
+}
+
+impl RemowtListener {
+ pub async fn accept(self) -> Result<RemowtStream> {
+ match self {
+ RemowtListener::Ssh(rx) => {
+ let ch = rx
+ .await
+ .map_err(|_| anyhow!("agent never connected the forwarded socket"))?;
+ Ok(RemowtStream::Ssh(ch.into_stream()))
+ }
+ RemowtListener::Local(listener, path) => {
+ let (stream, _) = listener.accept().await?;
+ let _ = std::fs::remove_file(&path);
+ Ok(RemowtStream::Local(stream))
+ }
+ RemowtListener::Iroh(rx) => rx
+ .await
+ .map_err(|_| anyhow!("agent never opened the iroh tunnel")),
+ }
+ }
+}
+
+pub enum RemowtStream {
+ Ssh(ChannelStream<Msg>),
+ Local(UnixStream),
+ Iroh(IrohBiStream),
+}
+
+impl AsyncRead for RemowtStream {
+ fn poll_read(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &mut ReadBuf<'_>,
+ ) -> Poll<io::Result<()>> {
+ match self.get_mut() {
+ RemowtStream::Ssh(s) => Pin::new(s).poll_read(cx, buf),
+ RemowtStream::Local(s) => Pin::new(s).poll_read(cx, buf),
+ RemowtStream::Iroh(s) => Pin::new(s).poll_read(cx, buf),
+ }
+ }
+}
+
+impl AsyncWrite for RemowtStream {
+ fn poll_write(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &[u8],
+ ) -> Poll<io::Result<usize>> {
+ match self.get_mut() {
+ RemowtStream::Ssh(s) => Pin::new(s).poll_write(cx, buf),
+ RemowtStream::Local(s) => Pin::new(s).poll_write(cx, buf),
+ RemowtStream::Iroh(s) => Pin::new(s).poll_write(cx, buf),
+ }
+ }
+
+ fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ match self.get_mut() {
+ RemowtStream::Ssh(s) => Pin::new(s).poll_flush(cx),
+ RemowtStream::Local(s) => Pin::new(s).poll_flush(cx),
+ RemowtStream::Iroh(s) => Pin::new(s).poll_flush(cx),
+ }
+ }
+
+ fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ match self.get_mut() {
+ RemowtStream::Ssh(s) => Pin::new(s).poll_shutdown(cx),
+ RemowtStream::Local(s) => Pin::new(s).poll_shutdown(cx),
+ RemowtStream::Iroh(s) => Pin::new(s).poll_shutdown(cx),
+ }
+ }
+}
remowt/crates/remowt-client/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-client/src/lib.rs
@@ -0,0 +1,714 @@
+use std::collections::HashMap;
+use std::env;
+use std::path::PathBuf;
+use std::sync::atomic::AtomicU64;
+use std::sync::{Arc, Mutex};
+
+use anyhow::{anyhow, bail, ensure, Context as _, Result};
+use bifrostlink::declarative::RemoteEndpoints;
+use bifrostlink::{Remote, Rpc, Rtt};
+use camino::{Utf8Path, Utf8PathBuf};
+use remowt_link_shared::iroh_tunnel::{DatagramRouter, IrohBiStream, TunnelAddr};
+use remowt_link_shared::plugin::PluginEndpointsClient;
+use remowt_link_shared::port::child_port;
+use remowt_link_shared::{Address, BifConfig};
+use russh::client::{connect, Config, Handle, Handler, Msg, Session};
+use russh::keys::agent::client::AgentClient;
+use russh::keys::agent::AgentIdentity;
+use russh::keys::check_known_hosts;
+use russh::keys::ssh_key::PublicKey;
+use russh::Channel;
+use tempfile::TempDir;
+use tokio::io::AsyncRead;
+use tokio::net::UnixListener;
+use tokio::sync::oneshot;
+use tokio::task::JoinHandle;
+use tokio::{
+ fs,
+ io::{AsyncBufReadExt as _, AsyncReadExt as _, AsyncWriteExt as _, BufReader},
+};
+use tracing::{debug, info, warn};
+use uuid::Uuid;
+
+pub mod editor;
+mod forwarded;
+mod shell;
+mod ssh_exec;
+mod subprocess;
+
+use self::ssh_exec::SshExecChild;
+pub use self::subprocess::{RemowtChild, SpawnOptions, StderrMode, StdioMode};
+pub use forwarded::{RemowtListener, RemowtStream};
+pub use shell::{RemowtShell, RemowtShellResizer};
+
+type Subs = Arc<Mutex<HashMap<Utf8PathBuf, oneshot::Sender<Channel<Msg>>>>>;
+
+fn sh_quote(s: impl AsRef<str>) -> String {
+ format!("'{}'", s.as_ref().replace('\'', "'\\''"))
+}
+
+const ESCALATORS: [(&str, &[&str]); 2] = [("run0", &["--background=", "--pipe"]), ("sudo", &[])];
+
+pub struct AgentBundle {
+ dir: PathBuf,
+ hashes: HashMap<String, String>,
+}
+
+impl AgentBundle {
+ pub fn from_dir(dir: impl Into<PathBuf>) -> Result<Self> {
+ let dir = dir.into();
+ let hashes_path = dir.join("hashes");
+ let raw = std::fs::read_to_string(&hashes_path)
+ .with_context(|| format!("reading agent hashes at {}", hashes_path.display()))?;
+ let mut hashes = HashMap::new();
+ for line in raw.lines() {
+ let line = line.trim();
+ if line.is_empty() {
+ continue;
+ }
+ let (arch, hash) = line
+ .split_once(char::is_whitespace)
+ .ok_or_else(|| anyhow!("malformed hashes line: {line:?}"))?;
+ hashes.insert(arch.to_owned(), hash.trim().to_owned());
+ }
+ ensure!(
+ !hashes.is_empty(),
+ "agent bundle {} has no hashes",
+ dir.display()
+ );
+ Ok(Self { dir, hashes })
+ }
+
+ fn binary(&self, arch: &str) -> PathBuf {
+ self.dir.join(format!("remowt-agent-{arch}"))
+ }
+
+ fn local_binary(&self) -> Result<PathBuf> {
+ let arch = env::consts::ARCH;
+ let path = self.binary(arch);
+ ensure!(
+ path.is_file(),
+ "no local remowt-agent build for arch {arch} in bundle {}",
+ self.dir.display()
+ );
+ Ok(path)
+ }
+}
+
+async fn run(sess: &Handle<SshHandler>, cmd: &str) -> Result<(Option<u32>, Vec<u8>)> {
+ let ch = sess.channel_open_session().await?;
+ ch.exec(true, cmd).await?;
+
+ let mut child = SshExecChild::from_exec(ch);
+ drop(child.stdin);
+ drain_to_tracing(child.stderr, cmd.to_owned(), true);
+
+ let mut out = Vec::new();
+ child.stdout.read_to_end(&mut out).await?;
+ let code = child.exit.await.ok().flatten();
+ Ok((code, out))
+}
+
+async fn run_string_ok(sess: &Handle<SshHandler>, cmd: &str) -> Result<String> {
+ let (code, mut out) = run(sess, cmd).await?;
+ ensure!(
+ code == Some(0),
+ "remote command failed (exit {code:?}): {cmd}"
+ );
+ if !out.is_empty() {
+ ensure!(
+ out.ends_with(b"\n"),
+ "remote command was not newline-terminated: {cmd}: {out:?}"
+ );
+ out.pop();
+ }
+ String::from_utf8(out).context("expected utf8 output for command")
+}
+
+async fn deploy_agent(sess: &Handle<SshHandler>, bundle: &AgentBundle) -> Result<Utf8PathBuf> {
+ debug!("uname -a");
+ let arch = run_string_ok(sess, "uname -m").await?;
+ let hash = bundle
+ .hashes
+ .get(&arch)
+ .ok_or_else(|| anyhow!("no remowt-agent build for remote arch {arch:?}"))?;
+
+ debug!("get dir");
+ let cache = run_string_ok(sess, "echo \"$XDG_CACHE_HOME\"").await?;
+ let dir = if cache.is_empty() {
+ let home = run_string_ok(sess, "echo \"$HOME\"").await?;
+ ensure!(
+ !home.is_empty(),
+ "remote $HOME and $XDG_CACHE_HOME both empty"
+ );
+ Utf8PathBuf::from(home).join(".cache/remowt")
+ } else {
+ Utf8PathBuf::from(cache).join("remowt")
+ };
+ let path = dir.join(hash);
+
+ debug!("presence");
+ let (present, _) = run(sess, &format!("test -x {}", sh_quote(&path))).await?;
+ if present != Some(0) {
+ let bin = bundle.binary(&arch);
+ debug!("read");
+ let bytes = fs::read(&bin)
+ .await
+ .with_context(|| format!("reading agent binary {}", bin.display()))?;
+ debug!("upload");
+ upload_agent(sess, &dir, &path, bytes).await?;
+ }
+ Ok(path)
+}
+
+async fn upload_agent(
+ sess: &Handle<SshHandler>,
+ dir: &Utf8Path,
+ path: &Utf8Path,
+ bytes: Vec<u8>,
+) -> Result<()> {
+ debug!("mkdirp");
+ run_string_ok(sess, &format!("mkdir -p {}", sh_quote(dir))).await?;
+
+ let tmp = dir.join(format!("tmp.{}", Uuid::new_v4()));
+ let ch = sess.channel_open_session().await?;
+ debug!("cat");
+ ch.exec(true, format!("cat > {}", sh_quote(&tmp))).await?;
+
+ let mut child = SshExecChild::from_exec(ch);
+ child
+ .stdin
+ .write_all(&bytes)
+ .await
+ .context("sending agent binary")?;
+ child
+ .stdin
+ .shutdown()
+ .await
+ .context("sending agent binary")?;
+ let code = child.wait().await;
+ ensure!(code == Some(0), "agent upload failed (exit {code:?})");
+
+ debug!("chmod");
+ run_string_ok(sess, &format!("chmod 0755 {}", sh_quote(&tmp))).await?;
+ run_string_ok(
+ sess,
+ &format!("mv -f {} {}", sh_quote(&tmp), sh_quote(path)),
+ )
+ .await?;
+ Ok(())
+}
+
+pub struct SshHandler {
+ host: String,
+ port: u16,
+ subs: Subs,
+}
+impl Handler for SshHandler {
+ type Error = russh::Error;
+ async fn check_server_key(
+ &mut self,
+ server_public_key: &PublicKey,
+ ) -> Result<bool, Self::Error> {
+ Ok(check_known_hosts(&self.host, self.port, server_public_key)?)
+ }
+ async fn server_channel_open_forwarded_streamlocal(
+ &mut self,
+ channel: Channel<Msg>,
+ socket_path: &str,
+ _session: &mut Session,
+ ) -> Result<(), Self::Error> {
+ let Some(ch) = self
+ .subs
+ .lock()
+ .expect("lock")
+ .remove(&Utf8PathBuf::from(socket_path))
+ else {
+ return Err(russh::Error::WrongChannel);
+ };
+ let _ = ch.send(channel);
+ Ok(())
+ }
+}
+
+enum Transport {
+ Ssh {
+ sess: Arc<Handle<SshHandler>>,
+ subs: Subs,
+ runtime_dir: Utf8PathBuf,
+ agent_path: Utf8PathBuf,
+ },
+ Local {
+ agent_path: PathBuf,
+ runtime_dir: Utf8PathBuf,
+ },
+}
+
+struct RemowtInner {
+ transport: Transport,
+ rpc: Rpc<BifConfig>,
+ elevated: tokio::sync::OnceCell<()>,
+ #[allow(dead_code)]
+ children: Mutex<Vec<tokio::process::Child>>,
+ _runtime_tmp: Option<TempDir>,
+ user: String,
+ iroh: IrohState,
+}
+
+#[derive(Default)]
+struct IrohState {
+ conn: tokio::sync::OnceCell<iroh::endpoint::Connection>,
+ #[allow(dead_code)]
+ endpoint: Mutex<Option<iroh::Endpoint>>,
+ subs: Arc<Mutex<HashMap<u64, oneshot::Sender<RemowtStream>>>>,
+ next_token: AtomicU64,
+ router: Mutex<Option<Arc<DatagramRouter>>>,
+}
+
+#[derive(Clone)]
+pub struct Remowt(Arc<RemowtInner>);
+
+pub type RemowtRemote = Remote<BifConfig>;
+
+impl Remowt {
+ /// Connect to the remote host over ssh, detect the architecture and deploy the required
+ /// agent binary.
+ pub async fn connect(host: &str, bundle: &AgentBundle, remowt_user: String) -> Result<Self> {
+ let conf = russh_config::parse_home(host)?;
+ let port = conf.host_config.port.or(conf.port).unwrap_or(22);
+ let hostname = conf
+ .host_config
+ .hostname
+ .clone()
+ .unwrap_or_else(|| conf.host_name.clone());
+ let user = conf
+ .user
+ .clone()
+ .unwrap_or_else(|| env::var("USER").unwrap_or_else(|_| "root".to_owned()));
+
+ let subs: Subs = Arc::new(Mutex::new(HashMap::new()));
+ let mut sess = connect(
+ Arc::new(Config::default()),
+ (hostname.clone(), port),
+ SshHandler {
+ host: hostname,
+ port,
+ subs: subs.clone(),
+ },
+ )
+ .await?;
+
+ let mut agent = AgentClient::connect_env().await?;
+ let rsa_hash = sess.best_supported_rsa_hash().await?.flatten();
+ let mut authenticated = false;
+ for ident in agent.request_identities().await? {
+ let AgentIdentity::PublicKey { key, .. } = ident else {
+ continue;
+ };
+ if sess
+ .authenticate_publickey_with(user.clone(), key, rsa_hash, &mut agent)
+ .await?
+ .success()
+ {
+ authenticated = true;
+ break;
+ }
+ }
+ ensure!(authenticated, "ssh authentication failed");
+
+ let sess = Arc::new(sess);
+
+ debug!("deploying agent");
+ let agent_path = deploy_agent(&sess, bundle).await?;
+
+ debug!("runtime dir");
+ let runtime_dir = remote_runtime_dir(&sess).await?;
+
+ let rpc = Rpc::<BifConfig>::new(Address::User);
+
+ let cmd_chan = sess.channel_open_session().await?;
+ debug!("starting agent");
+ cmd_chan
+ .exec(true, format!("{} real-agent", sh_quote(&agent_path)))
+ .await?;
+
+ let child = SshExecChild::from_exec(cmd_chan);
+ drain_to_tracing(child.stderr, "agent".to_owned(), true);
+ rpc.add_direct(
+ Address::Agent,
+ child_port(child.stdout, child.stdin),
+ Rtt(0),
+ );
+
+ let remowt = Self(Arc::new(RemowtInner {
+ transport: Transport::Ssh {
+ sess,
+ subs,
+ runtime_dir,
+ agent_path,
+ },
+ rpc,
+ elevated: tokio::sync::OnceCell::new(),
+ children: Mutex::new(Vec::new()),
+ _runtime_tmp: None,
+ user: remowt_user,
+ iroh: IrohState::default(),
+ }));
+ remowt.setup_iroh().await;
+ Ok(remowt)
+ }
+
+ /// "Connect" to the local machine's agent, by starting the agent binary locally.
+ pub async fn connect_local(bundle: &AgentBundle, user: String) -> Result<Self> {
+ let agent_path = bundle.local_binary()?;
+ let mut child = tokio::process::Command::new(&agent_path)
+ .arg("real-agent")
+ .arg("--local")
+ .stdin(std::process::Stdio::piped())
+ .stdout(std::process::Stdio::piped())
+ .kill_on_drop(true)
+ .spawn()
+ .with_context(|| format!("spawning agent binary {}", agent_path.display()))?;
+ let stdin = child.stdin.take().expect("stdin piped");
+ let stdout = child.stdout.take().expect("stdout piped");
+
+ let rpc = Rpc::<BifConfig>::new(Address::User);
+ rpc.add_direct(Address::Agent, child_port(stdout, stdin), Rtt(0));
+
+ let (runtime_dir, runtime_tmp) = local_runtime_dir()?;
+
+ Ok(Self(Arc::new(RemowtInner {
+ transport: Transport::Local {
+ agent_path,
+ runtime_dir,
+ },
+ rpc,
+ elevated: tokio::sync::OnceCell::new(),
+ children: Mutex::new(vec![child]),
+ _runtime_tmp: runtime_tmp,
+ user,
+ iroh: IrohState::default(),
+ })))
+ }
+
+ /// Get the handle to the underlying russh session handle.
+ pub fn ssh(&self) -> Option<Arc<Handle<SshHandler>>> {
+ match &self.0.transport {
+ Transport::Ssh { sess, .. } => Some(sess.clone()),
+ Transport::Local { .. } => None,
+ }
+ }
+
+ pub fn rpc(&self) -> Rpc<BifConfig> {
+ self.0.rpc.clone()
+ }
+
+ pub async fn load_plugin(&self, id: u16, name: &str) -> Result<()> {
+ let client: PluginEndpointsClient<BifConfig> = self.endpoints();
+ client
+ .load_plugin(id, name.to_owned())
+ .await?
+ .map_err(|e| anyhow!("agent failed to load plugin: {e}"))
+ }
+ pub async fn run0_load_plugin_path(&self, id: u16, path: &str) -> Result<()> {
+ self.ensure_escalated().await?;
+ let client: PluginEndpointsClient<BifConfig> =
+ PluginEndpointsClient::wrap(self.0.rpc.remote(Address::AgentPrivileged));
+ client
+ .load_plugin_path(id, path.to_owned())
+ .await?
+ .map_err(|e| anyhow!("privileged agent failed to load plugin: {e}"))
+ }
+ pub fn plugin_endpoints<R: RemoteEndpoints<BifConfig>>(&self, id: u16) -> R {
+ R::wrap(self.0.rpc.remote(Address::Plugin(id)))
+ }
+
+ pub fn endpoints<R: RemoteEndpoints<BifConfig>>(&self) -> R {
+ R::wrap(self.0.rpc.remote(Address::Agent))
+ }
+ pub async fn run0_endpoints<R: RemoteEndpoints<BifConfig>>(&self) -> Result<R> {
+ self.ensure_escalated().await?;
+ Ok(R::wrap(self.0.rpc.remote(Address::AgentPrivileged)))
+ }
+
+ async fn ensure_escalated(&self) -> Result<()> {
+ self.0
+ .elevated
+ .get_or_try_init(|| async {
+ let (agent_path, local) = match &self.0.transport {
+ Transport::Ssh { agent_path, .. } => (agent_path.as_str().to_owned(), false),
+ Transport::Local { agent_path, .. } => (
+ agent_path
+ .to_str()
+ .ok_or_else(|| anyhow!("local agent path is not utf-8"))?
+ .to_owned(),
+ true,
+ ),
+ };
+
+ let (tool, flags) = self.detect_escalation().await?;
+ let mut args: Vec<String> = Vec::new();
+ args.push("-w".to_owned());
+ args.push(tool.to_owned());
+ args.extend(flags.iter().copied().map(str::to_owned));
+ if tool == "run0" {
+ args.push(format!(
+ "--unit={}-{}.service",
+ self.0.user,
+ Uuid::new_v4().simple()
+ ));
+ }
+ args.push(agent_path);
+ args.push("real-agent".to_owned());
+ args.push("--privileged".to_owned());
+ if local {
+ args.push("--local".to_owned());
+ }
+
+ let child = self
+ .spawn(SpawnOptions {
+ program: "setsid".to_owned(),
+ args,
+ stdin: StdioMode::Pipe,
+ stdout: StdioMode::Pipe,
+ stderr: StderrMode::Inherit,
+ ..Default::default()
+ })
+ .await
+ .context("spawning privileged agent")?;
+
+ let stdin = child
+ .stdin
+ .ok_or_else(|| anyhow!("privileged agent stdin missing"))?;
+ let stdout = child
+ .stdout
+ .ok_or_else(|| anyhow!("privileged agent stdout missing"))?;
+
+ let port = child_port(stdout, stdin);
+ self.0
+ .rpc
+ .add_direct(Address::AgentPrivileged, port, Rtt(0));
+ anyhow::Ok(())
+ })
+ .await?;
+ Ok(())
+ }
+
+ async fn detect_escalation(&self) -> Result<(&'static str, &'static [&'static str])> {
+ for (tool, flags) in ESCALATORS {
+ let probe = self
+ .spawn(SpawnOptions {
+ program: (*tool).to_owned(),
+ args: vec!["--version".to_owned()],
+ stdout: StdioMode::Null,
+ stderr: StderrMode::Null,
+ ..Default::default()
+ })
+ .await;
+ if let Ok(child) = probe {
+ let _ = child.wait().await;
+ return Ok((tool, flags));
+ }
+ }
+ bail!("no escalation tool found")
+ }
+
+ /// XDG_RUNTIME_DIR on the remote machine.
+ pub fn runtime_dir(&self) -> Utf8PathBuf {
+ match &self.0.transport {
+ Transport::Ssh { runtime_dir, .. } => runtime_dir.clone(),
+ Transport::Local { runtime_dir, .. } => runtime_dir.clone(),
+ }
+ }
+
+ /// Bind unix listener socket on the remote machine with auto-generated path, returning the path.
+ pub async fn bind_runtime_unix(&self, hint: &str) -> Result<(RemowtListener, Utf8PathBuf)> {
+ let sock = self
+ .runtime_dir()
+ .join(format!("remowt-{hint}-{}.sock", Uuid::new_v4()));
+ let listener = self.bind_unix(&sock).await?;
+ Ok((listener, sock))
+ }
+
+ /// Bind unix listener socket on the remote machine on the specified path.
+ pub async fn bind_unix(&self, path: &Utf8Path) -> Result<RemowtListener> {
+ match &self.0.transport {
+ Transport::Ssh { sess, subs, .. } => {
+ let (tx, rx) = oneshot::channel();
+ subs.lock().expect("lock").insert(path.to_owned(), tx);
+ sess.streamlocal_forward(path.to_owned()).await?;
+ Ok(RemowtListener::Ssh(rx))
+ }
+ Transport::Local { .. } => {
+ let _ = std::fs::remove_file(path);
+ Ok(RemowtListener::Local(
+ UnixListener::bind(path)?,
+ path.to_owned(),
+ ))
+ }
+ }
+ }
+
+ /// Bind a data tunnel, preferring the iroh fast path when it is up. Escalated tunnels
+ /// (the privileged agent, a separate process with no iroh connection of its own) and the
+ /// local transport always use the ssh/unix path.
+ pub async fn bind_fast_tunnel(
+ &self,
+ hint: &str,
+ escalated: bool,
+ ) -> Result<(RemowtListener, TunnelAddr)> {
+ if !escalated && self.0.iroh.conn.get().is_some() {
+ let token = self
+ .0
+ .iroh
+ .next_token
+ .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
+ let (tx, rx) = oneshot::channel();
+ self.0.iroh.subs.lock().expect("lock").insert(token, tx);
+ return Ok((RemowtListener::Iroh(rx), TunnelAddr::Iroh { token }));
+ }
+ let (listener, path) = self.bind_runtime_unix(hint).await?;
+ Ok((listener, TunnelAddr::Unix(path)))
+ }
+
+ async fn setup_iroh(&self) {
+ if std::env::var_os("REMOWT_NO_IROH").is_some() {
+ debug!("REMOWT_NO_IROH set, skipping iroh fast tunnel");
+ return;
+ }
+ if !matches!(self.0.transport, Transport::Ssh { .. }) {
+ return;
+ }
+ if let Err(e) = self.try_setup_iroh().await {
+ warn!("iroh fast tunnel unavailable, using ssh: {e}");
+ }
+ }
+
+ async fn try_setup_iroh(&self) -> Result<()> {
+ use remowt_endpoints::iroh_tunnel::IrohTunnelClient;
+ use remowt_link_shared::iroh_tunnel::{build_endpoint, ssh_custom_addr, REMOWT_ALPN};
+
+ let (listener, sock) = self.bind_runtime_unix("iroh-xport").await?;
+ let secret = iroh::SecretKey::generate();
+ let client_id = secret.public();
+
+ let client: IrohTunnelClient<BifConfig> =
+ IrohTunnelClient::wrap(self.0.rpc.remote(Address::Agent));
+ let (accepted, agent_id) = tokio::join!(listener.accept(), client.setup(client_id, sock));
+ let stream = accepted?;
+ let agent_id = agent_id?.map_err(|e| anyhow!("agent iroh setup failed: {e}"))?;
+
+ let ep = build_endpoint(secret, stream, agent_id, false).await?;
+ let addr = iroh::EndpointAddr::from_parts(
+ agent_id,
+ [iroh::TransportAddr::Custom(ssh_custom_addr(agent_id))],
+ );
+ let conn = ep.connect(addr, REMOWT_ALPN).await?;
+ ensure!(conn.remote_id() == agent_id, "iroh peer identity mismatch");
+ info!("iroh fast tunnel established");
+
+ let subs = self.0.iroh.subs.clone();
+ let accept_conn = conn.clone();
+ tokio::spawn(async move {
+ loop {
+ match accept_conn.accept_bi().await {
+ Ok((send, mut recv)) => {
+ let subs = subs.clone();
+ tokio::spawn(async move {
+ let mut buf = [0u8; 8];
+ if tokio::io::AsyncReadExt::read_exact(&mut recv, &mut buf)
+ .await
+ .is_err()
+ {
+ return;
+ }
+ let token = u64::from_be_bytes(buf);
+ let tx = subs.lock().expect("lock").remove(&token);
+ if let Some(tx) = tx {
+ let _ = tx.send(RemowtStream::Iroh(IrohBiStream::new(send, recv)));
+ }
+ });
+ }
+ Err(e) => {
+ debug!("iroh accept loop ended: {e}");
+ break;
+ }
+ }
+ }
+ });
+
+ let log_conn = conn.clone();
+ tokio::spawn(async move {
+ tokio::time::sleep(std::time::Duration::from_secs(2)).await;
+ for p in log_conn.paths().iter() {
+ if p.is_selected() {
+ info!(rtt = ?p.rtt(), "iroh selected path: {}", p.remote_addr());
+ }
+ }
+ });
+
+ *self.0.iroh.router.lock().expect("lock") = Some(DatagramRouter::spawn(conn.clone()));
+ *self.0.iroh.endpoint.lock().expect("lock") = Some(ep);
+ let _ = self.0.iroh.conn.set(conn);
+ Ok(())
+ }
+
+ pub fn datagram_router(&self) -> Option<Arc<DatagramRouter>> {
+ self.0.iroh.router.lock().expect("lock").clone()
+ }
+}
+
+pub(crate) fn drain_to_tracing(
+ stream: impl AsyncRead + Unpin + 'static + Send,
+ context: String,
+ stderr: bool,
+) -> JoinHandle<()> {
+ tokio::spawn(async move {
+ let mut reader = BufReader::new(stream);
+ let mut buf = Vec::with_capacity(4096);
+ loop {
+ buf.clear();
+ match reader.read_until(b'\n', &mut buf).await {
+ Ok(0) => break,
+ Ok(_) => {
+ let line = String::from_utf8_lossy(buf.strip_suffix(b"\n").unwrap_or(&buf));
+ if stderr {
+ warn!(context = %context, "{line}");
+ } else {
+ info!(context = %context, "{line}");
+ }
+ }
+ Err(e) => {
+ warn!(context = %context, "child stdio read failed: {e}");
+ break;
+ }
+ }
+ }
+ })
+}
+
+fn local_runtime_dir() -> Result<(Utf8PathBuf, Option<TempDir>)> {
+ if let Ok(dir) = env::var("XDG_RUNTIME_DIR") {
+ if !dir.is_empty() {
+ return Ok((Utf8PathBuf::from(dir), None));
+ }
+ }
+ let tmp = tempfile::Builder::new()
+ .prefix("remowt.")
+ .rand_bytes(12)
+ .tempdir()?;
+ let dir = Utf8PathBuf::from_path_buf(tmp.path().to_owned())
+ .map_err(|p| anyhow!("temp dir {} is not utf-8", p.display()))?;
+ Ok((dir, Some(tmp)))
+}
+
+async fn remote_runtime_dir(sess: &Handle<SshHandler>) -> Result<Utf8PathBuf> {
+ let dir = run_string_ok(sess, "echo \"$XDG_RUNTIME_DIR\"").await?;
+ let dir = dir.trim();
+ if dir.is_empty() {
+ let tmp = run_string_ok(sess, "mktemp -d remowt.XXXXXXXXXXXX --tmpdir").await?;
+ Ok(Utf8PathBuf::from(tmp))
+ } else {
+ Ok(Utf8PathBuf::from(dir))
+ }
+}
remowt/crates/remowt-client/src/shell.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-client/src/shell.rs
@@ -0,0 +1,65 @@
+use anyhow::{anyhow, Result};
+use bifrostlink::declarative::RemoteEndpoints as _;
+use bifrostlink::Remote;
+use remowt_endpoints::pty::{PtyClient, ShellId};
+use remowt_link_shared::{Address, BifConfig};
+
+use crate::forwarded::RemowtStream;
+use crate::Remowt;
+
+pub struct RemowtShell {
+ pub id: ShellId,
+ pub stream: RemowtStream,
+ remote: Remote<BifConfig>,
+}
+impl RemowtShell {
+ pub fn resizer(&self) -> RemowtShellResizer {
+ RemowtShellResizer {
+ remote: self.remote.clone(),
+ id: self.id,
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct RemowtShellResizer {
+ remote: Remote<BifConfig>,
+ id: ShellId,
+}
+
+impl RemowtShellResizer {
+ pub async fn resize(&self, cols: u16, rows: u16) -> Result<()> {
+ PtyClient::wrap(self.remote.clone())
+ .resize(self.id, cols, rows)
+ .await?
+ .map_err(|e| anyhow!("failed to resize remote shell: {e}"))
+ }
+}
+
+impl Remowt {
+ pub async fn open_shell(
+ &self,
+ term: &str,
+ cols: u16,
+ rows: u16,
+ escalate: bool,
+ ) -> Result<RemowtShell> {
+ let (forwarded, tunnel) = self.bind_fast_tunnel("shell", escalate).await?;
+ let client: PtyClient<BifConfig> = if escalate {
+ self.run0_endpoints().await?
+ } else {
+ self.endpoints()
+ };
+ let id = client
+ .open_shell(tunnel, term.to_owned(), cols, rows)
+ .await?
+ .map_err(|e| anyhow!("agent failed to open shell: {e}"))?;
+ let stream = forwarded.accept().await?;
+
+ Ok(RemowtShell {
+ id,
+ stream,
+ remote: self.0.rpc.remote(Address::Agent),
+ })
+ }
+}
remowt/crates/remowt-client/src/ssh_exec.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-client/src/ssh_exec.rs
@@ -0,0 +1,82 @@
+use bytes::Bytes;
+use russh::client::Msg;
+use russh::{Channel, ChannelMsg};
+use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _, DuplexStream};
+use tokio::sync::oneshot;
+
+const BUF: usize = 64 * 1024;
+
+pub(crate) struct SshExecChild {
+ pub stdin: DuplexStream,
+ pub stdout: DuplexStream,
+ pub stderr: DuplexStream,
+ pub exit: oneshot::Receiver<Option<u32>>,
+}
+
+impl SshExecChild {
+ /// Manage channel returned by russh exec().
+ pub(crate) fn from_exec(ch: Channel<Msg>) -> Self {
+ let (stdin, mut stdin_r) = tokio::io::duplex(BUF);
+ let (mut out_w, stdout) = tokio::io::duplex(BUF);
+ let (mut err_w, stderr) = tokio::io::duplex(BUF);
+ let (exit_tx, exit) = oneshot::channel();
+
+ tokio::spawn(async move {
+ let (mut read, write) = ch.split();
+
+ let stdin_pump = tokio::spawn(async move {
+ let mut buf = vec![0u8; BUF];
+ loop {
+ match stdin_r.read(&mut buf).await {
+ Ok(0) | Err(_) => break,
+ Ok(n) => {
+ if write
+ .data_bytes(Bytes::copy_from_slice(&buf[..n]))
+ .await
+ .is_err()
+ {
+ return;
+ }
+ }
+ }
+ }
+ let _ = write.eof().await;
+ });
+
+ let mut code = None;
+ while let Some(msg) = read.wait().await {
+ match msg {
+ ChannelMsg::Data { data } => {
+ if out_w.write_all(&data).await.is_err() {
+ break;
+ }
+ }
+ ChannelMsg::ExtendedData { data, .. } => {
+ if err_w.write_all(&data).await.is_err() {
+ break;
+ }
+ }
+ ChannelMsg::ExitStatus { exit_status } => code = Some(exit_status),
+ _ => {}
+ }
+ }
+
+ stdin_pump.abort();
+ let _ = out_w.shutdown().await;
+ let _ = err_w.shutdown().await;
+ let _ = exit_tx.send(code);
+ });
+
+ SshExecChild {
+ stdin,
+ stdout,
+ stderr,
+ exit,
+ }
+ }
+
+ /// Wait for the process to finish, returning its exit status.
+ pub(crate) async fn wait(self) -> Option<u32> {
+ self.exit.await.ok().flatten()
+ }
+}
remowt/crates/remowt-client/src/subprocess.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-client/src/subprocess.rs
@@ -0,0 +1,439 @@
+use std::pin::pin;
+
+use anyhow::{anyhow, bail, Result};
+use camino::Utf8PathBuf;
+use futures::StreamExt as _;
+use remowt_endpoints::subprocess::{ProcId, SpawnSpec, StderrSpec, StdioSpec, SubprocessClient};
+use remowt_link_shared::iroh_tunnel::TunnelAddr;
+use remowt_link_shared::BifConfig;
+use serde::de::DeserializeOwned;
+use tokio::io::AsyncWriteExt as _;
+use tokio::select;
+use tokio_util::codec::{BytesCodec, FramedRead, LinesCodec};
+use tracing::{debug, info, warn};
+
+use crate::forwarded::{RemowtListener, RemowtStream};
+use crate::{drain_to_tracing, Remowt};
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub enum StdioMode {
+ #[default]
+ Null,
+ Pipe,
+ Inherit,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub enum StderrMode {
+ #[default]
+ Null,
+ Pipe,
+ Inherit,
+ MergeWithStdout,
+}
+
+#[derive(Default)]
+pub struct SpawnOptions {
+ pub program: String,
+ pub args: Vec<String>,
+ pub env: Vec<(String, String)>,
+ pub env_clear: bool,
+ pub cwd: Option<Utf8PathBuf>,
+ pub escalated: bool,
+ pub stdin: StdioMode,
+ pub stdout: StdioMode,
+ pub stderr: StderrMode,
+}
+
+pub struct RemowtChild {
+ pub stdin: Option<RemowtStream>,
+ pub stdout: Option<RemowtStream>,
+ pub stderr: Option<RemowtStream>,
+ id: ProcId,
+ client: SubprocessClient<BifConfig>,
+}
+
+impl RemowtChild {
+ pub async fn wait(self) -> Result<Option<i32>> {
+ let RemowtChild {
+ stdin,
+ stdout,
+ stderr,
+ id,
+ client,
+ } = self;
+ drop(stdin);
+ let drain_out = async move {
+ if let Some(s) = stdout {
+ let _ = drain_to_tracing(s, "<child stdout>".to_owned(), false).await;
+ }
+ };
+ let drain_err = async move {
+ if let Some(s) = stderr {
+ let _ = drain_to_tracing(s, "<child stderr>".to_owned(), true).await;
+ }
+ };
+ let wait = async move {
+ client
+ .wait(id)
+ .await?
+ .map_err(|e| anyhow!("agent wait failed: {e}"))
+ };
+ let (code, _, _) = tokio::join!(wait, drain_out, drain_err);
+ code
+ }
+
+ pub async fn kill(&self, signal: i32) -> Result<()> {
+ self.client
+ .kill(self.id, signal)
+ .await?
+ .map_err(|e| anyhow!("agent kill failed: {e}"))
+ }
+}
+
+fn needs_socket(m: StdioMode) -> bool {
+ matches!(m, StdioMode::Pipe | StdioMode::Inherit)
+}
+
+fn stderr_needs_socket(m: StderrMode) -> bool {
+ matches!(m, StderrMode::Pipe | StderrMode::Inherit)
+}
+
+impl Remowt {
+ pub async fn spawn(&self, opts: SpawnOptions) -> Result<RemowtChild> {
+ let SpawnOptions {
+ program,
+ args,
+ env,
+ env_clear,
+ cwd,
+ escalated,
+ stdin,
+ stdout,
+ stderr,
+ } = opts;
+
+ if matches!(stderr, StderrMode::MergeWithStdout) && !needs_socket(stdout) {
+ bail!("stderr=MergeWithStdout requires stdout=Pipe or Inherit");
+ }
+
+ let stdin_bound = if needs_socket(stdin) {
+ Some(self.bind_fast_tunnel("proc-stdin", escalated).await?)
+ } else {
+ None
+ };
+ let stdout_bound = if needs_socket(stdout) {
+ Some(self.bind_fast_tunnel("proc-stdout", escalated).await?)
+ } else {
+ None
+ };
+ let stderr_bound = if stderr_needs_socket(stderr) {
+ Some(self.bind_fast_tunnel("proc-stderr", escalated).await?)
+ } else {
+ None
+ };
+
+ let stdin_spec = match &stdin_bound {
+ Some((_, t)) => StdioSpec::Tunnel(t.clone()),
+ None => StdioSpec::Null,
+ };
+ let stdout_spec = match &stdout_bound {
+ Some((_, t)) => StdioSpec::Tunnel(t.clone()),
+ None => StdioSpec::Null,
+ };
+ let stderr_spec = match (&stderr, &stderr_bound) {
+ (StderrMode::Pipe | StderrMode::Inherit, Some((_, t))) => StderrSpec::Tunnel(t.clone()),
+ (StderrMode::MergeWithStdout, _) => StderrSpec::MergeWithStdout,
+ _ => StderrSpec::Null,
+ };
+
+ let client: SubprocessClient<BifConfig> = if escalated {
+ // Boxed to break the async-fn type cycle
+ Box::pin(self.run0_endpoints::<SubprocessClient<BifConfig>>()).await?
+ } else {
+ self.endpoints()
+ };
+
+ let spec = SpawnSpec {
+ program: program.clone(),
+ args,
+ env,
+ env_clear,
+ cwd,
+ stdin: stdin_spec,
+ stdout: stdout_spec,
+ stderr: stderr_spec,
+ };
+ let id = client
+ .spawn(spec)
+ .await?
+ .map_err(|e| anyhow!("agent spawn failed: {e}"))?;
+
+ let (stdin_res, stdout_res, stderr_res) = tokio::join!(
+ accept(stdin_bound),
+ accept(stdout_bound),
+ accept(stderr_bound),
+ );
+
+ let stdin_stream = handle_stdin(stdin, stdin_res?, &program);
+ let stdout_stream = handle_output(stdout, stdout_res?, &program);
+ let stderr_stream = handle_output_err(stderr, stderr_res?, &program);
+
+ Ok(RemowtChild {
+ stdin: stdin_stream,
+ stdout: stdout_stream,
+ stderr: stderr_stream,
+ id,
+ client,
+ })
+ }
+
+ pub fn cmd(&self, program: impl AsRef<str>) -> RemowtCommand {
+ let program = program.as_ref().to_owned();
+ RemowtCommand {
+ program,
+ args: vec![],
+ env: vec![],
+ remowt: self.clone(),
+ escalated: false,
+ }
+ }
+}
+
+async fn accept(b: Option<(RemowtListener, TunnelAddr)>) -> Result<Option<RemowtStream>> {
+ match b {
+ Some((l, _)) => Ok(Some(l.accept().await?)),
+ None => Ok(None),
+ }
+}
+
+fn handle_stdin(mode: StdioMode, s: Option<RemowtStream>, program: &str) -> Option<RemowtStream> {
+ match mode {
+ StdioMode::Pipe => s,
+ StdioMode::Inherit => {
+ if let Some(s) = s {
+ let program = program.to_owned();
+ tokio::spawn(async move {
+ let mut stdin = tokio::io::stdin();
+ let mut s = s;
+ if let Err(e) = tokio::io::copy(&mut stdin, &mut s).await {
+ warn!(program, "stdin forward ended: {e}");
+ }
+ let _ = s.shutdown().await;
+ });
+ }
+ None
+ }
+ StdioMode::Null => None,
+ }
+}
+
+fn handle_output(mode: StdioMode, s: Option<RemowtStream>, program: &str) -> Option<RemowtStream> {
+ match mode {
+ StdioMode::Pipe => s,
+ StdioMode::Inherit => {
+ if let Some(s) = s {
+ let program = program.to_owned();
+ tokio::spawn(drain_to_tracing(s, program, false));
+ }
+ None
+ }
+ StdioMode::Null => None,
+ }
+}
+
+fn handle_output_err(
+ mode: StderrMode,
+ s: Option<RemowtStream>,
+ program: &str,
+) -> Option<RemowtStream> {
+ match mode {
+ StderrMode::Pipe => s,
+ StderrMode::Inherit => {
+ if let Some(s) = s {
+ let program = program.to_owned();
+ tokio::spawn(drain_to_tracing(s, program, true));
+ }
+ None
+ }
+ StderrMode::MergeWithStdout | StderrMode::Null => None,
+ }
+}
+
+fn escape_bash(input: &str, out: &mut String) {
+ const TO_ESCAPE: &str = "$ !\"#&'()*,;<>?[\\]^`{|}";
+ if input.chars().all(|c| !TO_ESCAPE.contains(c)) {
+ out.push_str(input);
+ return;
+ }
+ out.push('\'');
+ for (i, v) in input.split('\'').enumerate() {
+ if i != 0 {
+ out.push_str("'\"'\"'");
+ }
+ out.push_str(v);
+ }
+ out.push('\'');
+}
+
+#[derive(Clone)]
+pub struct RemowtCommand {
+ program: String,
+ args: Vec<String>,
+ env: Vec<(String, String)>,
+ remowt: Remowt,
+ escalated: bool,
+}
+
+impl RemowtCommand {
+ pub fn arg(&mut self, arg: impl AsRef<str>) -> &mut Self {
+ self.args.push(arg.as_ref().to_owned());
+ self
+ }
+ pub fn args<V: AsRef<str>>(&mut self, args: impl IntoIterator<Item = V>) -> &mut Self {
+ for arg in args {
+ self.args.push(arg.as_ref().to_owned());
+ }
+ self
+ }
+ pub fn eqarg(&mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> &mut Self {
+ self.args
+ .push(format!("{}={}", key.as_ref(), value.as_ref()));
+ self
+ }
+ pub fn comparg(&mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> &mut Self {
+ self.args.push(key.as_ref().to_owned());
+ self.args.push(value.as_ref().to_owned());
+ self
+ }
+ pub fn env(&mut self, name: impl AsRef<str>, value: impl AsRef<str>) -> &mut Self {
+ self.env
+ .push((name.as_ref().to_owned(), value.as_ref().to_owned()));
+ self
+ }
+
+ pub fn sudo(mut self) -> Self {
+ self.escalated = true;
+ self
+ }
+
+ /// Only for display.
+ fn shell_line(&self) -> String {
+ let mut out = String::new();
+ if self.escalated {
+ out.push_str("run0 ");
+ }
+ if !self.env.is_empty() {
+ out.push_str("env");
+ for (k, v) in &self.env {
+ out.push(' ');
+ assert!(!k.contains('='));
+ escape_bash(k, &mut out);
+ out.push('=');
+ escape_bash(v, &mut out);
+ }
+ out.push(' ');
+ }
+ escape_bash(&self.program, &mut out);
+ for arg in &self.args {
+ out.push(' ');
+ escape_bash(arg, &mut out);
+ }
+ out
+ }
+
+ fn into_spawn_options(self) -> (Remowt, SpawnOptions, String) {
+ let line = self.shell_line();
+ let opts = SpawnOptions {
+ program: self.program,
+ args: self.args,
+ env: self.env,
+ env_clear: false,
+ cwd: None,
+ escalated: self.escalated,
+ stdin: StdioMode::Null,
+ stdout: StdioMode::Pipe,
+ stderr: StderrMode::Pipe,
+ };
+ (self.remowt, opts, line)
+ }
+
+ pub async fn run(self) -> Result<()> {
+ run_inner(self, false).await.map(|_| ())
+ }
+ pub async fn run_string(self) -> Result<String> {
+ let bytes = run_inner(self, true).await?.expect("want_stdout");
+ Ok(String::from_utf8(bytes)?)
+ }
+ pub async fn run_value<T: DeserializeOwned>(self) -> Result<T> {
+ let s = self.run_string().await?;
+ Ok(serde_json::from_str(&s)?)
+ }
+}
+
+async fn run_inner(cmd: RemowtCommand, want_stdout: bool) -> Result<Option<Vec<u8>>> {
+ let (remowt, opts, line) = cmd.into_spawn_options();
+ debug!("running command {line:?} over remowt");
+ let program = opts.program.clone();
+ let mut child = remowt.spawn(opts).await?;
+ let stderr = child.stderr.take().expect("stderr=Pipe");
+ let stdout = child.stdout.take().expect("stdout=Pipe");
+
+ let mut err = FramedRead::new(stderr, LinesCodec::new());
+ let (mut out_bytes, mut out_lines) = if want_stdout {
+ (Some(FramedRead::new(stdout, BytesCodec::new())), None)
+ } else {
+ (None, Some(FramedRead::new(stdout, LinesCodec::new())))
+ };
+
+ let mut buf = if want_stdout { Some(Vec::new()) } else { None };
+
+ let mut wait = pin!(child.wait());
+ let exit = loop {
+ select! {
+ biased;
+
+ Some(e) = err.next() => {
+ let e = e?;
+ warn!(program = %program, "{e}");
+ }
+ Some(o) = async { out_bytes.as_mut()?.next().await }, if want_stdout => {
+ buf.as_mut().expect("want_stdout").extend_from_slice(&o?);
+ }
+ Some(o) = async { out_lines.as_mut()?.next().await }, if !want_stdout => {
+ let o = o?;
+ info!(program = %program, "{o}");
+ }
+ res = &mut wait => {
+ break res?;
+ }
+ }
+ };
+
+ while let Some(e) = err.next().await {
+ if let Ok(line) = e {
+ warn!(program = %program, "{line}");
+ }
+ }
+ if want_stdout {
+ if let Some(out_bytes) = out_bytes.as_mut() {
+ while let Some(o) = out_bytes.next().await {
+ if let Ok(chunk) = o {
+ buf.as_mut().expect("want_stdout").extend_from_slice(&chunk);
+ }
+ }
+ }
+ } else if let Some(out_lines) = out_lines.as_mut() {
+ while let Some(o) = out_lines.next().await {
+ if let Ok(line) = o {
+ info!(program = %program, "{line}");
+ }
+ }
+ }
+
+ match exit {
+ Some(0) => Ok(buf),
+ Some(c) => bail!("command '{line}' failed with status {c}"),
+ None => Err(anyhow!("command '{line}' ended without an exit status")),
+ }
+}
remowt/crates/remowt-endpoints/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-endpoints/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "remowt-endpoints"
+description = "Nix daemon proxy"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+bifrostlink.workspace = true
+camino.workspace = true
+serde = { workspace = true }
+tempfile.workspace = true
+thiserror.workspace = true
+tokio = { workspace = true, features = [
+ "net",
+ "io-util",
+ "rt",
+ "process",
+ "io-std",
+] }
+tracing.workspace = true
+nix = { workspace = true, features = ["process", "signal", "term"] }
+zbus.workspace = true
+remowt-link-shared.workspace = true
+iroh.workspace = true
remowt/crates/remowt-endpoints/src/forward.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-endpoints/src/forward.rs
@@ -0,0 +1,117 @@
+use std::collections::HashMap;
+use std::net::SocketAddr;
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::{Arc, Mutex};
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use remowt_link_shared::iroh_tunnel::{TunnelAddr, TunnelDialer};
+use serde::{Deserialize, Serialize};
+use std::result::Result;
+use tokio::net::{TcpStream, UdpSocket};
+use tracing::warn;
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("tunnel unavailable: {0}")]
+ Tunnel(String),
+ #[error("connect to {0} failed: {1}")]
+ Connect(String, String),
+ #[error("invalid target address {0:?}")]
+ BadAddr(String),
+ #[error("udp forward requires the iroh fast path, which is not established")]
+ NoIroh,
+}
+
+#[derive(Clone)]
+pub struct Forward {
+ dialer: Arc<TunnelDialer>,
+ next_session: Arc<AtomicU64>,
+}
+
+impl Forward {
+ pub fn new(dialer: Arc<TunnelDialer>) -> Self {
+ Self {
+ dialer,
+ next_session: Arc::new(AtomicU64::new(0)),
+ }
+ }
+}
+
+#[endpoints(ns = 12)]
+impl Forward {
+ #[endpoints(id = 1)]
+ async fn connect_tcp(&self, tunnel: TunnelAddr, addr: String) -> Result<(), Error> {
+ let stream = self
+ .dialer
+ .connect_tunnel(&tunnel)
+ .await
+ .map_err(|e| Error::Tunnel(e.to_string()))?;
+ let tcp = TcpStream::connect(&addr)
+ .await
+ .map_err(|e| Error::Connect(addr, e.to_string()))?;
+ tokio::spawn(async move {
+ let mut stream = stream;
+ let mut tcp = tcp;
+ let _ = tokio::io::copy_bidirectional(&mut stream, &mut tcp).await;
+ });
+ Ok(())
+ }
+
+ #[endpoints(id = 2)]
+ async fn open_udp(&self, addr: String) -> Result<u64, Error> {
+ let target: SocketAddr = addr.parse().map_err(|_| Error::BadAddr(addr.clone()))?;
+ let router = self.dialer.router().ok_or(Error::NoIroh)?;
+ let session = self.next_session.fetch_add(1, Ordering::Relaxed);
+ let mut rx = router.register(session);
+
+ let sockets: Arc<Mutex<HashMap<u64, Arc<UdpSocket>>>> =
+ Arc::new(Mutex::new(HashMap::new()));
+ tokio::spawn(async move {
+ while let Some((sub, payload)) = rx.recv().await {
+ let existing = sockets.lock().expect("lock").get(&sub).cloned();
+ let sock = match existing {
+ Some(s) => s,
+ None => {
+ let sock = match UdpSocket::bind(unspecified_for(&target)).await {
+ Ok(s) => s,
+ Err(e) => {
+ warn!("udp forward: bind failed: {e}");
+ continue;
+ }
+ };
+ if let Err(e) = sock.connect(target).await {
+ warn!("udp forward: connect {target} failed: {e}");
+ continue;
+ }
+ let sock = Arc::new(sock);
+ sockets.lock().expect("lock").insert(sub, sock.clone());
+ // Reply reader: datagrams from the target go back on (session, sub).
+ let router = router.clone();
+ let reply_sock = sock.clone();
+ tokio::spawn(async move {
+ let mut buf = vec![0u8; 65535];
+ while let Ok(n) = reply_sock.recv(&mut buf).await {
+ if router.send(session, sub, &buf[..n]).is_err() {
+ break;
+ }
+ }
+ });
+ sock
+ }
+ };
+ let _ = sock.send(&payload).await;
+ }
+ router.unregister(session);
+ });
+
+ Ok(session)
+ }
+}
+
+fn unspecified_for(target: &SocketAddr) -> SocketAddr {
+ match target {
+ SocketAddr::V4(_) => SocketAddr::from(([0, 0, 0, 0], 0)),
+ SocketAddr::V6(_) => SocketAddr::from(([0u16; 8], 0)),
+ }
+}
remowt/crates/remowt-endpoints/src/fs.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-endpoints/src/fs.rs
@@ -0,0 +1,105 @@
+use std::io::ErrorKind;
+use std::str::FromStr;
+use std::sync::Mutex;
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use camino::Utf8PathBuf;
+use serde::{Deserialize, Serialize};
+use tempfile::TempDir;
+
+#[derive(Default)]
+pub struct Fs {
+ tempdirs: Mutex<Vec<TempDir>>,
+}
+
+impl Fs {
+ pub fn new() -> Self {
+ Self::default()
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("file not found")]
+ NotFound,
+ #[error("file name/contents is not utf8")]
+ InvalidUtf8,
+ #[error("unknown fs error")]
+ Unknown,
+}
+
+#[endpoints(ns = 1)]
+impl Fs {
+ #[endpoints(id = 1)]
+ async fn read_file_tiny(&self, path: Utf8PathBuf) -> Result<Vec<u8>, Error> {
+ match tokio::fs::read(path).await {
+ Ok(v) => Ok(v),
+ Err(e) if e.kind() == ErrorKind::NotFound => Err(Error::NotFound),
+ _ => Err(Error::Unknown),
+ }
+ }
+ #[endpoints(id = 2)]
+ async fn file_exists(&self, path: Utf8PathBuf) -> bool {
+ tokio::fs::try_exists(path).await.unwrap_or(false)
+ }
+ #[endpoints(id = 3)]
+ async fn read_dir_raw(&self, path: Utf8PathBuf) -> Result<Vec<Utf8PathBuf>, Error> {
+ let mut dir = match tokio::fs::read_dir(path).await {
+ Ok(dir) => dir,
+ Err(e) if e.kind() == ErrorKind::NotFound => return Err(Error::NotFound),
+ Err(_) => return Err(Error::Unknown),
+ };
+ let mut out = Vec::new();
+ while let Ok(Some(entry)) = dir.next_entry().await {
+ let name = Utf8PathBuf::try_from(entry.file_name()).map_err(|_| Error::InvalidUtf8)?;
+ out.push(name);
+ }
+ Ok(out)
+ }
+ #[endpoints(id = 4)]
+ async fn mktemp_dir_raw(&self) -> Result<Utf8PathBuf, Error> {
+ let dir = tempfile::Builder::new()
+ .prefix("remowt.")
+ .tempdir()
+ .map_err(|_| Error::Unknown)?;
+ let mut tempdirs = self.tempdirs.lock().expect("not poisoned");
+ let path = Utf8PathBuf::try_from(dir.path().to_owned()).map_err(|_| Error::InvalidUtf8);
+ tempdirs.push(dir);
+ path
+ }
+ #[endpoints(id = 5)]
+ async fn rm_file(&self, path: Utf8PathBuf) -> Result<(), Error> {
+ match tokio::fs::remove_file(path).await {
+ Ok(()) => Ok(()),
+ Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
+ Err(_) => Err(Error::Unknown),
+ }
+ }
+}
+
+impl<C: Config> FsClient<C> {
+ pub async fn read_file_text(&self, path: impl Into<Utf8PathBuf>) -> Result<String, Error> {
+ let v = self
+ .read_file_tiny(path.into())
+ .await
+ .map_err(|_| Error::Unknown)?;
+ let v = v?;
+ String::from_utf8(v).map_err(|_| Error::InvalidUtf8)
+ }
+ pub async fn read_file_value<T: FromStr>(
+ &self,
+ path: impl Into<Utf8PathBuf>,
+ ) -> Result<Result<T, T::Err>, Error> {
+ let text = self.read_file_text(path).await?;
+ Ok(T::from_str(&text))
+ }
+ pub async fn mktemp_dir(&self) -> Result<Utf8PathBuf, Error> {
+ self.mktemp_dir_raw().await.map_err(|_| Error::Unknown)?
+ }
+ pub async fn read_dir(&self, path: impl Into<Utf8PathBuf>) -> Result<Vec<Utf8PathBuf>, Error> {
+ self.read_dir_raw(path.into())
+ .await
+ .map_err(|_| Error::Unknown)?
+ }
+}
remowt/crates/remowt-endpoints/src/iroh_tunnel.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-endpoints/src/iroh_tunnel.rs
@@ -0,0 +1,85 @@
+use std::sync::{Arc, Mutex};
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use camino::Utf8PathBuf;
+use iroh::{Endpoint, EndpointId, SecretKey};
+use remowt_link_shared::iroh_tunnel::{build_endpoint, TunnelDialer};
+use serde::{Deserialize, Serialize};
+use std::result::Result;
+use tokio::net::UnixStream;
+use tracing::{debug, warn};
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("iroh transport pipe unavailable: {0}")]
+ Pipe(String),
+ #[error("iroh endpoint failed: {0}")]
+ Iroh(String),
+}
+
+#[derive(Clone)]
+pub struct IrohTunnel {
+ dialer: Arc<TunnelDialer>,
+ endpoint: Arc<Mutex<Option<Endpoint>>>,
+}
+
+impl IrohTunnel {
+ pub fn new(dialer: Arc<TunnelDialer>) -> Self {
+ Self {
+ dialer,
+ endpoint: Arc::new(Mutex::new(None)),
+ }
+ }
+}
+
+#[endpoints(ns = 11)]
+impl IrohTunnel {
+ #[endpoints(id = 1)]
+ async fn setup(
+ &self,
+ client_id: EndpointId,
+ xport_socket: Utf8PathBuf,
+ ) -> Result<EndpointId, Error> {
+ let stream = UnixStream::connect(&xport_socket)
+ .await
+ .map_err(|e| Error::Pipe(e.to_string()))?;
+
+ let secret = SecretKey::generate();
+ let agent_id = secret.public();
+ let ep = build_endpoint(secret, stream, client_id, true)
+ .await
+ .map_err(|e| Error::Iroh(e.to_string()))?;
+
+ let dialer = self.dialer.clone();
+ let accept_ep = ep.clone();
+ tokio::spawn(async move {
+ while let Some(incoming) = accept_ep.accept().await {
+ let dialer = dialer.clone();
+ match incoming.accept() {
+ Ok(accepting) => {
+ tokio::spawn(async move {
+ match accepting.await {
+ Ok(conn) => {
+ if conn.remote_id() != client_id {
+ warn!("iroh: rejecting connection from unexpected peer");
+ conn.close(0u32.into(), b"unexpected peer");
+ return;
+ }
+ debug!("iroh tunnel connection accepted");
+ dialer.set_conn(conn);
+ }
+ Err(e) => warn!("iroh accept failed: {e}"),
+ }
+ });
+ }
+ Err(e) => warn!("iroh incoming rejected: {e}"),
+ }
+ }
+ });
+
+ *self.endpoint.lock().expect("not poisoned") = Some(ep);
+
+ Ok(agent_id)
+ }
+}
remowt/crates/remowt-endpoints/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-endpoints/src/lib.rs
@@ -0,0 +1,7 @@
+pub mod forward;
+pub mod fs;
+pub mod iroh_tunnel;
+pub mod nix_daemon;
+pub mod pty;
+pub mod subprocess;
+pub mod systemd;
remowt/crates/remowt-endpoints/src/nix_daemon.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-endpoints/src/nix_daemon.rs
@@ -0,0 +1,59 @@
+use std::process::Stdio;
+use std::sync::Arc;
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use remowt_link_shared::iroh_tunnel::{TunnelAddr, TunnelDialer};
+use serde::{Deserialize, Serialize};
+use std::result::Result;
+use tokio::process::Command;
+
+#[derive(Clone)]
+pub struct NixDaemon {
+ dialer: Arc<TunnelDialer>,
+}
+
+impl NixDaemon {
+ pub fn new(dialer: Arc<TunnelDialer>) -> Self {
+ Self { dialer }
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("nix daemon unavailable: {0}")]
+ DaemonUnavailable(String),
+ #[error("tunnel socket unavailable: {0}")]
+ Tunnel(String),
+}
+
+#[endpoints(ns = 4)]
+impl NixDaemon {
+ #[endpoints(id = 2)]
+ async fn serve_store(&self, store: String, tunnel: TunnelAddr) -> Result<(), Error> {
+ let mut child = Command::new("nix-daemon")
+ .arg("--stdio")
+ .arg("--store")
+ .arg(&store)
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .spawn()
+ .map_err(|e| Error::DaemonUnavailable(e.to_string()))?;
+ let tunnel = self
+ .dialer
+ .connect_tunnel(&tunnel)
+ .await
+ .map_err(|e| Error::Tunnel(e.to_string()))?;
+ let mut stdin = child.stdin.take().expect("piped");
+ let mut stdout = child.stdout.take().expect("piped");
+ tokio::spawn(async move {
+ let (mut tr, mut tw) = tokio::io::split(tunnel);
+ let _ = tokio::join!(
+ tokio::io::copy(&mut tr, &mut stdin),
+ tokio::io::copy(&mut stdout, &mut tw),
+ );
+ let _ = child.wait().await;
+ });
+ Ok(())
+ }
+}
remowt/crates/remowt-endpoints/src/pty.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-endpoints/src/pty.rs
@@ -0,0 +1,260 @@
+use std::collections::HashMap;
+use std::io;
+use std::os::fd::{AsRawFd, OwnedFd};
+use std::pin::Pin;
+use std::process::Stdio;
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll};
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use nix::libc;
+use nix::pty::{openpty, OpenptyResult, Winsize};
+use remowt_link_shared::iroh_tunnel::{TunnelAddr, TunnelDialer};
+use serde::{Deserialize, Serialize};
+use tokio::io::unix::AsyncFd;
+use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
+use tracing::{debug, info, warn};
+
+pub type ShellId = u64;
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("openpty failed: {0}")]
+ Open(String),
+ #[error("failed to spawn shell: {0}")]
+ Spawn(String),
+ #[error("failed to connect to forwarded socket: {0}")]
+ Connect(String),
+ #[error("no shell with that id")]
+ NoSuchShell,
+ #[error("resize failed: {0}")]
+ Resize(String),
+ #[error("io error: {0}")]
+ Io(String),
+}
+
+impl From<io::Error> for Error {
+ fn from(e: io::Error) -> Self {
+ Error::Io(e.to_string())
+ }
+}
+
+#[derive(Clone)]
+pub struct Pty {
+ shells: Arc<Mutex<HashMap<ShellId, OwnedFd>>>,
+ next_id: Arc<AtomicU64>,
+ dialer: Arc<TunnelDialer>,
+}
+
+impl Pty {
+ pub fn new(dialer: Arc<TunnelDialer>) -> Self {
+ Self {
+ shells: Default::default(),
+ next_id: Default::default(),
+ dialer,
+ }
+ }
+}
+
+#[endpoints(ns = 7)]
+impl Pty {
+ #[endpoints(id = 1)]
+ async fn open_shell(
+ &self,
+ tunnel: TunnelAddr,
+ term: String,
+ cols: u16,
+ rows: u16,
+ ) -> Result<ShellId, Error> {
+ let ws = Winsize {
+ ws_row: rows,
+ ws_col: cols,
+ ws_xpixel: 0,
+ ws_ypixel: 0,
+ };
+ let OpenptyResult { master, slave } =
+ openpty(Some(&ws), None).map_err(|e| Error::Open(e.to_string()))?;
+
+ let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_owned());
+
+ let slave_in = slave.try_clone()?;
+ let slave_out = slave.try_clone()?;
+ let slave_err = slave;
+
+ let mut cmd = tokio::process::Command::new(&shell);
+ cmd.env("TERM", &term);
+ if let Ok(home) = std::env::var("HOME") {
+ cmd.current_dir(home);
+ }
+ cmd.stdin(Stdio::from(slave_in));
+ cmd.stdout(Stdio::from(slave_out));
+ cmd.stderr(Stdio::from(slave_err));
+ // SAFETY: only async-signal-safe calls (setsid, ioctl) before exec.
+ unsafe {
+ cmd.pre_exec(|| {
+ nix::unistd::setsid().map_err(|e| io::Error::from_raw_os_error(e as i32))?;
+ if libc::ioctl(0, libc::TIOCSCTTY as _, 0) < 0 {
+ return Err(io::Error::last_os_error());
+ }
+ Ok(())
+ });
+ }
+
+ let mut child = cmd.spawn().map_err(|e| Error::Spawn(e.to_string()))?;
+
+ let resize_fd = master.try_clone()?;
+ let id = self.next_id.fetch_add(1, Ordering::Relaxed);
+ self.shells
+ .lock()
+ .expect("not poisoned")
+ .insert(id, resize_fd);
+
+ let sock = match self.dialer.connect_tunnel(&tunnel).await {
+ Ok(s) => s,
+ Err(e) => {
+ self.shells.lock().expect("not poisoned").remove(&id);
+ let _ = child.kill().await;
+ return Err(Error::Connect(e.to_string()));
+ }
+ };
+ let pty = AsyncPty::new(master)?;
+
+ debug!(id, shell, "shell opened");
+ let shells = self.shells.clone();
+ tokio::spawn(async move {
+ let mut pty = pty;
+ let mut sock = sock;
+ if let Err(e) = tokio::io::copy_bidirectional(&mut pty, &mut sock).await {
+ warn!(id, "shell pump ended: {e}");
+ }
+ let _ = child.kill().await;
+ shells.lock().expect("not poisoned").remove(&id);
+ info!(id, "shell closed");
+ });
+
+ Ok(id)
+ }
+
+ #[endpoints(id = 2)]
+ async fn resize(&self, id: ShellId, cols: u16, rows: u16) -> Result<(), Error> {
+ let ws = libc::winsize {
+ ws_row: rows,
+ ws_col: cols,
+ ws_xpixel: 0,
+ ws_ypixel: 0,
+ };
+ let shells = self.shells.lock().expect("not poisoned");
+ let fd = shells.get(&id).ok_or(Error::NoSuchShell)?;
+ // SAFETY: `fd` is a live PTY master
+ let rc = unsafe { libc::ioctl(fd.as_raw_fd(), libc::TIOCSWINSZ as _, &ws) };
+ if rc < 0 {
+ return Err(Error::Resize(io::Error::last_os_error().to_string()));
+ }
+ Ok(())
+ }
+}
+
+struct AsyncPty {
+ fd: AsyncFd<OwnedFd>,
+}
+
+impl AsyncPty {
+ fn new(fd: OwnedFd) -> io::Result<Self> {
+ let raw = fd.as_raw_fd();
+ // SAFETY: standard F_GETFL/F_SETFL round-trip on a valid fd.
+ unsafe {
+ let flags = libc::fcntl(raw, libc::F_GETFL);
+ if flags < 0 {
+ return Err(io::Error::last_os_error());
+ }
+ if libc::fcntl(raw, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
+ return Err(io::Error::last_os_error());
+ }
+ }
+ Ok(Self {
+ fd: AsyncFd::new(fd)?,
+ })
+ }
+}
+
+impl AsyncRead for AsyncPty {
+ fn poll_read(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &mut ReadBuf<'_>,
+ ) -> Poll<io::Result<()>> {
+ let this = self.get_mut();
+ loop {
+ let mut guard = match this.fd.poll_read_ready(cx) {
+ Poll::Ready(Ok(g)) => g,
+ Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
+ Poll::Pending => return Poll::Pending,
+ };
+ let unfilled = buf.initialize_unfilled();
+ let res = guard.try_io(|inner| {
+ let fd = inner.get_ref().as_raw_fd();
+ // SAFETY: writing into `unfilled`'s own backing storage.
+ let n = unsafe { libc::read(fd, unfilled.as_mut_ptr().cast(), unfilled.len()) };
+ if n < 0 {
+ let err = io::Error::last_os_error();
+ if err.raw_os_error() == Some(libc::EIO) {
+ Ok(0)
+ } else {
+ Err(err)
+ }
+ } else {
+ Ok(n as usize)
+ }
+ });
+ match res {
+ Ok(Ok(n)) => {
+ buf.advance(n);
+ return Poll::Ready(Ok(()));
+ }
+ Ok(Err(e)) => return Poll::Ready(Err(e)),
+ Err(_would_block) => continue,
+ }
+ }
+ }
+}
+
+impl AsyncWrite for AsyncPty {
+ fn poll_write(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &[u8],
+ ) -> Poll<io::Result<usize>> {
+ let this = self.get_mut();
+ loop {
+ let mut guard = match this.fd.poll_write_ready(cx) {
+ Poll::Ready(Ok(g)) => g,
+ Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
+ Poll::Pending => return Poll::Pending,
+ };
+ let res = guard.try_io(|inner| {
+ let fd = inner.get_ref().as_raw_fd();
+ // SAFETY: reading from `buf` for `buf.len()` bytes.
+ let n = unsafe { libc::write(fd, buf.as_ptr().cast(), buf.len()) };
+ if n < 0 {
+ Err(io::Error::last_os_error())
+ } else {
+ Ok(n as usize)
+ }
+ });
+ match res {
+ Ok(r) => return Poll::Ready(r),
+ Err(_would_block) => continue,
+ }
+ }
+ }
+
+ fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ Poll::Ready(Ok(()))
+ }
+
+ fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ Poll::Ready(Ok(()))
+ }
+}
remowt/crates/remowt-endpoints/src/subprocess.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-endpoints/src/subprocess.rs
@@ -0,0 +1,293 @@
+use std::collections::HashMap;
+use std::io;
+use std::process::Stdio;
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::{Arc, Mutex};
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use camino::Utf8PathBuf;
+use nix::sys::signal::{self, Signal};
+use nix::unistd::Pid;
+use remowt_link_shared::iroh_tunnel::{TunnelAddr, TunnelDialer, TunnelStream};
+use serde::{Deserialize, Serialize};
+use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
+use tokio::process::{ChildStderr, ChildStdout, Command};
+use tokio::sync::{mpsc, watch};
+use tracing::{debug, warn};
+
+pub type ProcId = u64;
+
+#[derive(Serialize, Deserialize, Debug)]
+pub enum StdioSpec {
+ Null,
+ Tunnel(TunnelAddr),
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub enum StderrSpec {
+ Null,
+ Tunnel(TunnelAddr),
+ MergeWithStdout,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct SpawnSpec {
+ pub program: String,
+ pub args: Vec<String>,
+ pub env: Vec<(String, String)>,
+ pub env_clear: bool,
+ pub cwd: Option<Utf8PathBuf>,
+ pub stdin: StdioSpec,
+ pub stdout: StdioSpec,
+ pub stderr: StderrSpec,
+}
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("spawn failed: {0}")]
+ Spawn(String),
+ #[error("connect to forwarded socket failed: {0}")]
+ Connect(String),
+ #[error("no process with that id")]
+ NoSuchProcess,
+ #[error("MergeWithStdout requires stdout=Socket")]
+ BadMerge,
+ #[error("invalid signal: {0}")]
+ BadSignal(i32),
+ #[error("kill failed: {0}")]
+ Kill(String),
+ #[error("io error: {0}")]
+ Io(String),
+}
+
+impl From<io::Error> for Error {
+ fn from(e: io::Error) -> Self {
+ Error::Io(e.to_string())
+ }
+}
+
+struct ChildState {
+ pid: u32,
+ exit_rx: watch::Receiver<Option<Option<i32>>>,
+}
+
+#[derive(Clone)]
+pub struct Subprocess {
+ children: Arc<Mutex<HashMap<ProcId, ChildState>>>,
+ next_id: Arc<AtomicU64>,
+ dialer: Arc<TunnelDialer>,
+}
+
+impl Subprocess {
+ pub fn new(dialer: Arc<TunnelDialer>) -> Self {
+ Self {
+ children: Default::default(),
+ next_id: Default::default(),
+ dialer,
+ }
+ }
+}
+
+#[endpoints(ns = 10)]
+impl Subprocess {
+ #[endpoints(id = 1)]
+ async fn spawn(&self, spec: SpawnSpec) -> Result<ProcId, Error> {
+ let SpawnSpec {
+ program,
+ args,
+ env,
+ env_clear,
+ cwd,
+ stdin,
+ stdout,
+ stderr,
+ } = spec;
+
+ if matches!(stderr, StderrSpec::MergeWithStdout) && !matches!(stdout, StdioSpec::Tunnel(_))
+ {
+ return Err(Error::BadMerge);
+ }
+
+ let mut cmd = Command::new(&program);
+ cmd.args(&args);
+ if env_clear {
+ cmd.env_clear();
+ }
+ for (k, v) in &env {
+ cmd.env(k, v);
+ }
+ if let Some(cwd) = &cwd {
+ cmd.current_dir(cwd);
+ }
+ cmd.stdin(match &stdin {
+ StdioSpec::Tunnel(_) => Stdio::piped(),
+ StdioSpec::Null => Stdio::null(),
+ });
+ cmd.stdout(match &stdout {
+ StdioSpec::Tunnel(_) => Stdio::piped(),
+ StdioSpec::Null => Stdio::null(),
+ });
+ cmd.stderr(match &stderr {
+ StderrSpec::Tunnel(_) | StderrSpec::MergeWithStdout => Stdio::piped(),
+ StderrSpec::Null => Stdio::null(),
+ });
+ cmd.kill_on_drop(false);
+
+ let mut child = cmd.spawn().map_err(|e| Error::Spawn(e.to_string()))?;
+ let pid = child
+ .id()
+ .ok_or_else(|| Error::Spawn("child exited before pid available".to_owned()))?;
+
+ if let StdioSpec::Tunnel(addr) = &stdin {
+ let sock = self
+ .dialer
+ .connect_tunnel(addr)
+ .await
+ .map_err(|e| Error::Connect(e.to_string()))?;
+ let mut stdin_w = child.stdin.take().expect("piped");
+ tokio::spawn(async move {
+ let (mut sr, _) = tokio::io::split(sock);
+ let _ = tokio::io::copy(&mut sr, &mut stdin_w).await;
+ let _ = stdin_w.shutdown().await;
+ });
+ }
+
+ let stdout_handle = child.stdout.take();
+ let stderr_handle = child.stderr.take();
+
+ match (&stdout, &stderr, stdout_handle, stderr_handle) {
+ (StdioSpec::Tunnel(out_addr), StderrSpec::MergeWithStdout, Some(out), Some(err)) => {
+ let sock = self
+ .dialer
+ .connect_tunnel(out_addr)
+ .await
+ .map_err(|e| Error::Connect(e.to_string()))?;
+ tokio::spawn(merge_to_sock(out, err, sock));
+ }
+ (StdioSpec::Tunnel(out_addr), _, Some(out), err_opt) => {
+ let sock = self
+ .dialer
+ .connect_tunnel(out_addr)
+ .await
+ .map_err(|e| Error::Connect(e.to_string()))?;
+ tokio::spawn(pump_to_sock(out, sock));
+ if let (StderrSpec::Tunnel(err_addr), Some(err)) = (&stderr, err_opt) {
+ let err_sock = self
+ .dialer
+ .connect_tunnel(err_addr)
+ .await
+ .map_err(|e| Error::Connect(e.to_string()))?;
+ tokio::spawn(pump_to_sock(err, err_sock));
+ }
+ }
+ (StdioSpec::Null, StderrSpec::Tunnel(err_addr), _, Some(err)) => {
+ let sock = self
+ .dialer
+ .connect_tunnel(err_addr)
+ .await
+ .map_err(|e| Error::Connect(e.to_string()))?;
+ tokio::spawn(pump_to_sock(err, sock));
+ }
+ _ => {}
+ }
+
+ let (exit_tx, exit_rx) = watch::channel(None);
+ let id = self.next_id.fetch_add(1, Ordering::Relaxed);
+ self.children
+ .lock()
+ .expect("not poisoned")
+ .insert(id, ChildState { pid, exit_rx });
+
+ debug!(id, pid, program, "subprocess spawned");
+ tokio::spawn(async move {
+ let result = child.wait().await;
+ let code = match result {
+ Ok(status) => status.code(),
+ Err(e) => {
+ warn!(id, "child.wait failed: {e}");
+ None
+ }
+ };
+ let _ = exit_tx.send(Some(code));
+ });
+
+ Ok(id)
+ }
+
+ #[endpoints(id = 2)]
+ async fn wait(&self, id: ProcId) -> Result<Option<i32>, Error> {
+ let mut rx = {
+ let map = self.children.lock().expect("not poisoned");
+ let entry = map.get(&id).ok_or(Error::NoSuchProcess)?;
+ entry.exit_rx.clone()
+ };
+ rx.wait_for(|v| v.is_some())
+ .await
+ .map_err(|_| Error::Io("exit channel closed".to_owned()))?;
+ let code = rx.borrow().flatten();
+ self.children.lock().expect("not poisoned").remove(&id);
+ Ok(code)
+ }
+
+ #[endpoints(id = 3)]
+ async fn kill(&self, id: ProcId, signal: i32) -> Result<(), Error> {
+ let pid = {
+ let map = self.children.lock().expect("not poisoned");
+ let entry = map.get(&id).ok_or(Error::NoSuchProcess)?;
+ entry.pid
+ };
+ let sig = Signal::try_from(signal).map_err(|_| Error::BadSignal(signal))?;
+ signal::kill(Pid::from_raw(pid as i32), sig).map_err(|e| Error::Kill(e.to_string()))?;
+ Ok(())
+ }
+}
+
+async fn pump_to_sock<R>(mut from: R, sock: TunnelStream)
+where
+ R: tokio::io::AsyncRead + Unpin,
+{
+ let (_, mut sw) = tokio::io::split(sock);
+ let _ = tokio::io::copy(&mut from, &mut sw).await;
+ let _ = sw.shutdown().await;
+}
+
+async fn merge_to_sock(mut stdout: ChildStdout, mut stderr: ChildStderr, sock: TunnelStream) {
+ let (_, mut sw) = tokio::io::split(sock);
+ let (tx, mut rx) = mpsc::channel::<Vec<u8>>(64);
+ let tx_out = tx.clone();
+ let out_pump = tokio::spawn(async move {
+ let mut buf = vec![0u8; 4096];
+ loop {
+ match stdout.read(&mut buf).await {
+ Ok(0) | Err(_) => break,
+ Ok(n) => {
+ if tx_out.send(buf[..n].to_vec()).await.is_err() {
+ break;
+ }
+ }
+ }
+ }
+ });
+ let err_pump = tokio::spawn(async move {
+ let mut buf = vec![0u8; 4096];
+ loop {
+ match stderr.read(&mut buf).await {
+ Ok(0) | Err(_) => break,
+ Ok(n) => {
+ if tx.send(buf[..n].to_vec()).await.is_err() {
+ break;
+ }
+ }
+ }
+ }
+ });
+ while let Some(chunk) = rx.recv().await {
+ if sw.write_all(&chunk).await.is_err() {
+ break;
+ }
+ }
+ let _ = out_pump.await;
+ let _ = err_pump.await;
+ let _ = sw.shutdown().await;
+}
remowt/crates/remowt-endpoints/src/systemd.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-endpoints/src/systemd.rs
@@ -0,0 +1,54 @@
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use serde::{Deserialize, Serialize};
+use zbus::proxy;
+use zbus::zvariant::OwnedObjectPath;
+
+pub struct Systemd;
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("systemd request failed: {0}")]
+ Failed(String),
+}
+
+#[proxy(
+ interface = "org.freedesktop.systemd1.Manager",
+ default_service = "org.freedesktop.systemd1",
+ default_path = "/org/freedesktop/systemd1"
+)]
+trait Manager {
+ fn start_unit(&self, name: &str, mode: &str) -> zbus::Result<OwnedObjectPath>;
+ fn stop_unit(&self, name: &str, mode: &str) -> zbus::Result<OwnedObjectPath>;
+}
+
+async fn manager() -> Result<ManagerProxy<'static>, Error> {
+ let conn = zbus::Connection::system()
+ .await
+ .map_err(|e| Error::Failed(e.to_string()))?;
+ ManagerProxy::new(&conn)
+ .await
+ .map_err(|e| Error::Failed(e.to_string()))
+}
+
+#[endpoints(ns = 5)]
+impl Systemd {
+ #[endpoints(id = 1)]
+ async fn start(&self, unit: String) -> Result<(), Error> {
+ manager()
+ .await?
+ .start_unit(&unit, "replace")
+ .await
+ .map_err(|e| Error::Failed(e.to_string()))?;
+ Ok(())
+ }
+ #[endpoints(id = 2)]
+ async fn stop(&self, unit: String) -> Result<(), Error> {
+ manager()
+ .await?
+ .stop_unit(&unit, "replace")
+ .await
+ .map_err(|e| Error::Failed(e.to_string()))?;
+ Ok(())
+ }
+}
remowt/crates/remowt-link-shared/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-link-shared/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "remowt-link-shared"
+description = "Shared bifrostlink endpoint wiring for remowt"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+bifrostlink.workspace = true
+bytes.workspace = true
+camino = { workspace = true }
+serde = { workspace = true, features = ["derive"] }
+serde_json.workspace = true
+thiserror.workspace = true
+tokio = { workspace = true, features = ["fs", "io-util", "macros", "net", "sync", "rt"] }
+tokio-util = { workspace = true, features = ["codec"] }
+tracing.workspace = true
+remowt-ui-prompt.workspace = true
+
+iroh.workspace = true
+iroh-base.workspace = true
+noq-udp.workspace = true
+n0-watcher.workspace = true
+futures.workspace = true
remowt/crates/remowt-link-shared/src/editor.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-link-shared/src/editor.rs
@@ -0,0 +1,45 @@
+use std::future::Future;
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::{Config, Rpc};
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("editor failed: {0}")]
+ Failed(String),
+}
+
+pub trait EditorBackend: Send + Sync {
+ fn open_editor(&self, socket_path: String) -> impl Future<Output = Result<(), Error>> + Send;
+ fn expose_tcp(&self, addr: String) -> impl Future<Output = Result<u16, Error>> + Send;
+ fn expose_udp(&self, addr: String) -> impl Future<Output = Result<u16, Error>> + Send;
+}
+
+pub struct EditorEndpoints<E>(pub E);
+
+#[endpoints(ns = 8)]
+impl<E: EditorBackend + 'static> EditorEndpoints<E> {
+ #[endpoints(id = 1)]
+ async fn open_editor(&self, socket_path: String) -> Result<(), Error> {
+ self.0.open_editor(socket_path).await
+ }
+
+ #[endpoints(id = 2)]
+ async fn expose_tcp(&self, addr: String) -> Result<u16, Error> {
+ self.0.expose_tcp(addr).await
+ }
+
+ #[endpoints(id = 3)]
+ async fn expose_udp(&self, addr: String) -> Result<u16, Error> {
+ self.0.expose_udp(addr).await
+ }
+}
+
+pub fn serve_editor<E, C>(rpc: &mut Rpc<C>, editor: E)
+where
+ E: EditorBackend + Send + Sync + 'static,
+ C: Config,
+{
+ EditorEndpoints(editor).register_endpoints(rpc);
+}
remowt/crates/remowt-link-shared/src/iroh_tunnel.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-link-shared/src/iroh_tunnel.rs
@@ -0,0 +1,536 @@
+use std::collections::HashMap;
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll};
+use std::time::Duration;
+use std::{fmt, io};
+
+use anyhow::Result;
+use bytes::{Bytes, BytesMut};
+use camino::Utf8PathBuf;
+use futures::{SinkExt as _, Stream};
+use iroh::endpoint::transports::{
+ CustomEndpoint, CustomSender, CustomTransport, RecvInfo, Transmit,
+};
+use iroh::endpoint::{presets, Connection, RecvStream, SendStream};
+use iroh::{Endpoint, EndpointId, SecretKey};
+use iroh_base::CustomAddr;
+use serde::{Deserialize, Serialize};
+use tokio::io::{AsyncRead, AsyncWrite, ReadBuf, ReadHalf, WriteHalf};
+use tokio::net::UnixStream;
+use tokio::sync::{mpsc, watch};
+use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec};
+use tracing::{debug, warn};
+
+// "rmwtssh"
+pub const REMOWT_SSH_TRANSPORT_ID: u64 = 0x_72_6d_77_74_73_73_68;
+
+pub const REMOWT_ALPN: &[u8] = b"remowt/tunnel/0";
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub enum TunnelAddr {
+ Unix(Utf8PathBuf),
+ Iroh { token: u64 },
+}
+
+pub fn ssh_custom_addr(id: EndpointId) -> CustomAddr {
+ CustomAddr::from((REMOWT_SSH_TRANSPORT_ID, &id.as_bytes()[..]))
+}
+
+pub async fn build_endpoint<S>(
+ secret: SecretKey,
+ stream: S,
+ remote: EndpointId,
+ accept: bool,
+) -> Result<Endpoint>
+where
+ S: AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static,
+{
+ let local = secret.public();
+ let transport = Arc::new(SshTransport::new(stream, local, remote));
+ let mut builder = Endpoint::builder(presets::N0)
+ .secret_key(secret)
+ .add_custom_transport(transport);
+ if accept {
+ builder = builder.alpns(vec![REMOWT_ALPN.to_vec()]);
+ }
+ Ok(builder.bind().await?)
+}
+
+struct SshTransport {
+ local: CustomAddr,
+ remote: CustomAddr,
+ addrs: n0_watcher::Watchable<Vec<CustomAddr>>,
+ endpoint: Mutex<Option<SshEndpointParts>>,
+ sender: Arc<SshSender>,
+}
+
+struct SshEndpointParts {
+ reader: Pin<Box<dyn FramedDatagrams>>,
+}
+
+trait FramedDatagrams: Stream<Item = io::Result<BytesMut>> + Send + Sync {}
+impl<T: Stream<Item = io::Result<BytesMut>> + Send + Sync> FramedDatagrams for T {}
+
+impl fmt::Debug for SshTransport {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("SshTransport").finish_non_exhaustive()
+ }
+}
+
+impl SshTransport {
+ fn new<S>(stream: S, local: EndpointId, remote: EndpointId) -> Self
+ where
+ S: AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static,
+ {
+ let (read, write): (ReadHalf<S>, WriteHalf<S>) = tokio::io::split(stream);
+ let reader = FramedRead::new(read, LengthDelimitedCodec::new());
+
+ let (tx, rx) = mpsc::unbounded_channel::<Bytes>();
+ tokio::spawn(writer_task(rx, write));
+
+ let local = ssh_custom_addr(local);
+ Self {
+ addrs: n0_watcher::Watchable::new(vec![local.clone()]),
+ local,
+ remote: ssh_custom_addr(remote),
+ endpoint: Mutex::new(Some(SshEndpointParts {
+ reader: Box::pin(reader),
+ })),
+ sender: Arc::new(SshSender { tx }),
+ }
+ }
+}
+
+async fn writer_task<W: AsyncWrite + Unpin>(mut rx: mpsc::UnboundedReceiver<Bytes>, write: W) {
+ let mut framed = FramedWrite::new(write, LengthDelimitedCodec::new());
+ while let Some(datagram) = rx.recv().await {
+ if let Err(e) = framed.send(datagram).await {
+ debug!("ssh transport writer ended: {e}");
+ break;
+ }
+ }
+}
+
+impl CustomTransport for SshTransport {
+ fn bind(&self) -> io::Result<Box<dyn CustomEndpoint>> {
+ let parts = self
+ .endpoint
+ .lock()
+ .expect("not poisoned")
+ .take()
+ .ok_or_else(|| io::Error::other("ssh transport already bound"))?;
+ Ok(Box::new(SshEndpoint {
+ local: self.local.clone(),
+ remote: self.remote.clone(),
+ addrs: self.addrs.clone(),
+ sender: self.sender.clone(),
+ reader: parts.reader,
+ }))
+ }
+}
+
+struct SshEndpoint {
+ local: CustomAddr,
+ remote: CustomAddr,
+ addrs: n0_watcher::Watchable<Vec<CustomAddr>>,
+ sender: Arc<SshSender>,
+ reader: Pin<Box<dyn FramedDatagrams>>,
+}
+
+impl fmt::Debug for SshEndpoint {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("SshEndpoint").finish_non_exhaustive()
+ }
+}
+
+impl CustomEndpoint for SshEndpoint {
+ fn watch_local_addrs(&self) -> n0_watcher::Direct<Vec<CustomAddr>> {
+ self.addrs.watch()
+ }
+
+ fn create_sender(&self) -> Arc<dyn CustomSender> {
+ self.sender.clone()
+ }
+
+ fn poll_recv(
+ &mut self,
+ cx: &mut Context,
+ bufs: &mut [io::IoSliceMut<'_>],
+ metas: &mut [noq_udp::RecvMeta],
+ recv_infos: &mut [RecvInfo],
+ ) -> Poll<io::Result<usize>> {
+ assert_eq!(bufs.len(), metas.len());
+ assert_eq!(bufs.len(), recv_infos.len());
+ if bufs.is_empty() {
+ return Poll::Ready(Ok(0));
+ }
+ let mut count = 0;
+ while count < bufs.len() {
+ match self.reader.as_mut().poll_next(cx) {
+ Poll::Ready(Some(Ok(frame))) => {
+ let buf = &mut bufs[count];
+ if buf.len() < frame.len() {
+ warn!("ssh transport datagram {} > buf {}", frame.len(), buf.len());
+ if count > 0 {
+ break;
+ }
+ return Poll::Ready(Err(io::Error::other("datagram exceeds recv buffer")));
+ }
+ buf[..frame.len()].copy_from_slice(&frame);
+ metas[count].len = frame.len();
+ metas[count].stride = frame.len();
+ recv_infos[count] =
+ RecvInfo::new(self.remote.clone(), Some(self.local.clone()));
+ count += 1;
+ }
+ Poll::Ready(Some(Err(e))) => {
+ if count > 0 {
+ break;
+ }
+ return Poll::Ready(Err(e));
+ }
+ Poll::Ready(None) => {
+ if count > 0 {
+ break;
+ }
+ return Poll::Ready(Err(io::Error::other("ssh transport closed")));
+ }
+ Poll::Pending => {
+ if count > 0 {
+ break;
+ }
+ return Poll::Pending;
+ }
+ }
+ }
+ Poll::Ready(Ok(count))
+ }
+}
+
+#[derive(Debug)]
+struct SshSender {
+ tx: mpsc::UnboundedSender<Bytes>,
+}
+
+impl CustomSender for SshSender {
+ fn is_valid_send_addr(&self, addr: &CustomAddr) -> bool {
+ addr.id() == REMOWT_SSH_TRANSPORT_ID
+ }
+
+ fn poll_send(
+ &self,
+ _cx: &mut Context,
+ _dst: &CustomAddr,
+ _src: Option<&CustomAddr>,
+ transmit: &Transmit<'_>,
+ ) -> Poll<io::Result<()>> {
+ let segment = transmit.segment_size.unwrap_or(transmit.contents.len());
+ if segment == 0 || transmit.contents.is_empty() {
+ return Poll::Ready(Ok(()));
+ }
+ for chunk in transmit.contents.chunks(segment) {
+ if self.tx.send(Bytes::copy_from_slice(chunk)).is_err() {
+ return Poll::Ready(Err(io::Error::other("ssh transport writer gone")));
+ }
+ }
+ Poll::Ready(Ok(()))
+ }
+}
+
+pub struct IrohBiStream {
+ send: SendStream,
+ recv: RecvStream,
+}
+
+impl IrohBiStream {
+ pub fn new(send: SendStream, recv: RecvStream) -> Self {
+ Self { send, recv }
+ }
+}
+
+impl AsyncRead for IrohBiStream {
+ fn poll_read(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &mut ReadBuf<'_>,
+ ) -> Poll<io::Result<()>> {
+ AsyncRead::poll_read(Pin::new(&mut self.get_mut().recv), cx, buf)
+ }
+}
+
+impl AsyncWrite for IrohBiStream {
+ fn poll_write(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &[u8],
+ ) -> Poll<io::Result<usize>> {
+ AsyncWrite::poll_write(Pin::new(&mut self.get_mut().send), cx, buf)
+ }
+ fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ AsyncWrite::poll_flush(Pin::new(&mut self.get_mut().send), cx)
+ }
+ fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ AsyncWrite::poll_shutdown(Pin::new(&mut self.get_mut().send), cx)
+ }
+}
+
+pub enum TunnelStream {
+ Unix(UnixStream),
+ Iroh(IrohBiStream),
+}
+
+impl AsyncRead for TunnelStream {
+ fn poll_read(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &mut ReadBuf<'_>,
+ ) -> Poll<io::Result<()>> {
+ match self.get_mut() {
+ TunnelStream::Unix(s) => Pin::new(s).poll_read(cx, buf),
+ TunnelStream::Iroh(s) => Pin::new(s).poll_read(cx, buf),
+ }
+ }
+}
+
+impl AsyncWrite for TunnelStream {
+ fn poll_write(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &[u8],
+ ) -> Poll<io::Result<usize>> {
+ match self.get_mut() {
+ TunnelStream::Unix(s) => Pin::new(s).poll_write(cx, buf),
+ TunnelStream::Iroh(s) => Pin::new(s).poll_write(cx, buf),
+ }
+ }
+ fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ match self.get_mut() {
+ TunnelStream::Unix(s) => Pin::new(s).poll_flush(cx),
+ TunnelStream::Iroh(s) => Pin::new(s).poll_flush(cx),
+ }
+ }
+ fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ match self.get_mut() {
+ TunnelStream::Unix(s) => Pin::new(s).poll_shutdown(cx),
+ TunnelStream::Iroh(s) => Pin::new(s).poll_shutdown(cx),
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct TunnelDialer {
+ conn: watch::Sender<Option<Connection>>,
+ router: Arc<Mutex<Option<Arc<DatagramRouter>>>>,
+}
+
+impl Default for TunnelDialer {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl TunnelDialer {
+ pub fn new() -> Self {
+ let (conn, _) = watch::channel(None);
+ Self {
+ conn,
+ router: Arc::new(Mutex::new(None)),
+ }
+ }
+
+ pub fn set_conn(&self, conn: Connection) {
+ *self.router.lock().expect("lock") = Some(DatagramRouter::spawn(conn.clone()));
+ self.conn.send_replace(Some(conn));
+ }
+
+ pub fn router(&self) -> Option<Arc<DatagramRouter>> {
+ self.router.lock().expect("lock").clone()
+ }
+
+ pub async fn connect_tunnel(&self, addr: &TunnelAddr) -> io::Result<TunnelStream> {
+ match addr {
+ TunnelAddr::Unix(path) => Ok(TunnelStream::Unix(UnixStream::connect(path).await?)),
+ TunnelAddr::Iroh { token } => {
+ let mut rx = self.conn.subscribe();
+ let conn =
+ tokio::time::timeout(Duration::from_secs(5), rx.wait_for(|c| c.is_some()))
+ .await
+ .map_err(|_| io::Error::other("timed out waiting for iroh connection"))?
+ .map_err(|_| io::Error::other("iroh connection channel closed"))?
+ .clone()
+ .expect("is_some");
+ let (mut send, recv) = conn.open_bi().await.map_err(io::Error::other)?;
+ send.write_all(&token.to_be_bytes())
+ .await
+ .map_err(io::Error::other)?;
+ Ok(TunnelStream::Iroh(IrohBiStream::new(send, recv)))
+ }
+ }
+ }
+}
+
+const DGRAM_HEADER: usize = 16;
+
+type DatagramRoutes = Arc<Mutex<HashMap<u64, mpsc::UnboundedSender<(u64, Bytes)>>>>;
+
+pub struct DatagramRouter {
+ conn: Connection,
+ routes: DatagramRoutes,
+}
+
+impl DatagramRouter {
+ pub fn spawn(conn: Connection) -> Arc<Self> {
+ let routes: DatagramRoutes = Arc::new(Mutex::new(HashMap::new()));
+ let read_routes = routes.clone();
+ let read_conn = conn.clone();
+ tokio::spawn(async move {
+ loop {
+ match read_conn.read_datagram().await {
+ Ok(dg) if dg.len() >= DGRAM_HEADER => {
+ let session = u64::from_be_bytes(dg[0..8].try_into().expect("8 bytes"));
+ let sub = u64::from_be_bytes(dg[8..16].try_into().expect("8 bytes"));
+ let payload = dg.slice(DGRAM_HEADER..);
+ let tx = read_routes.lock().expect("lock").get(&session).cloned();
+ if let Some(tx) = tx {
+ let _ = tx.send((sub, payload));
+ }
+ }
+ Ok(_) => {}
+ Err(e) => {
+ debug!("datagram read loop ended: {e}");
+ break;
+ }
+ }
+ }
+ });
+ Arc::new(Self { conn, routes })
+ }
+
+ pub fn register(&self, session: u64) -> mpsc::UnboundedReceiver<(u64, Bytes)> {
+ let (tx, rx) = mpsc::unbounded_channel();
+ self.routes.lock().expect("lock").insert(session, tx);
+ rx
+ }
+
+ pub fn unregister(&self, session: u64) {
+ self.routes.lock().expect("lock").remove(&session);
+ }
+
+ pub fn send(&self, session: u64, sub: u64, payload: &[u8]) -> io::Result<()> {
+ let mut buf = BytesMut::with_capacity(DGRAM_HEADER + payload.len());
+ buf.extend_from_slice(&session.to_be_bytes());
+ buf.extend_from_slice(&sub.to_be_bytes());
+ buf.extend_from_slice(payload);
+ self.conn
+ .send_datagram(buf.freeze())
+ .map_err(io::Error::other)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use iroh::endpoint::presets;
+ use iroh::{EndpointAddr, RelayMode, TransportAddr};
+
+ async fn endpoint<S>(secret: SecretKey, stream: S, remote: EndpointId, accept: bool) -> Endpoint
+ where
+ S: AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static,
+ {
+ let local = secret.public();
+ let transport = Arc::new(SshTransport::new(stream, local, remote));
+ let mut builder = Endpoint::builder(presets::N0)
+ .secret_key(secret)
+ .relay_mode(RelayMode::Disabled)
+ .clear_ip_transports()
+ .add_custom_transport(transport);
+ if accept {
+ builder = builder.alpns(vec![REMOWT_ALPN.to_vec()]);
+ }
+ builder.bind().await.expect("bind")
+ }
+
+ #[tokio::test]
+ async fn echo_over_ssh_transport() {
+ let (client_pipe, agent_pipe) = tokio::io::duplex(256 * 1024);
+ let client_secret = SecretKey::generate();
+ let agent_secret = SecretKey::generate();
+ let client_id = client_secret.public();
+ let agent_id = agent_secret.public();
+
+ let client = endpoint(client_secret, client_pipe, agent_id, false).await;
+ let agent = endpoint(agent_secret, agent_pipe, client_id, true).await;
+
+ let server = tokio::spawn(async move {
+ let incoming = agent.accept().await.expect("incoming");
+ let conn = incoming
+ .accept()
+ .expect("accept")
+ .await
+ .expect("connecting");
+ assert_eq!(conn.remote_id(), client_id);
+ let (mut send, mut recv) = conn.accept_bi().await.expect("accept_bi");
+ let mut buf = [0u8; 5];
+ recv.read_exact(&mut buf).await.expect("read");
+ send.write_all(&buf).await.expect("write");
+ send.finish().expect("finish");
+ conn.closed().await;
+ });
+
+ let addr =
+ EndpointAddr::from_parts(agent_id, [TransportAddr::Custom(ssh_custom_addr(agent_id))]);
+ let conn = client.connect(addr, REMOWT_ALPN).await.expect("connect");
+ assert_eq!(conn.remote_id(), agent_id);
+ let (mut send, mut recv) = conn.open_bi().await.expect("open_bi");
+ send.write_all(b"hello").await.expect("write");
+ send.finish().expect("finish");
+ let mut buf = [0u8; 5];
+ recv.read_exact(&mut buf).await.expect("read");
+ assert_eq!(&buf, b"hello");
+
+ conn.close(0u32.into(), b"done");
+ let _ = server.await;
+ }
+
+ #[tokio::test]
+ async fn dgram_round_trip() {
+ let (client_pipe, agent_pipe) = tokio::io::duplex(256 * 1024);
+ let client_secret = SecretKey::generate();
+ let agent_secret = SecretKey::generate();
+ let client_id = client_secret.public();
+ let agent_id = agent_secret.public();
+
+ let client = endpoint(client_secret, client_pipe, agent_id, false).await;
+ let agent = endpoint(agent_secret, agent_pipe, client_id, true).await;
+
+ let server = tokio::spawn(async move {
+ let incoming = agent.accept().await.expect("incoming");
+ let conn = incoming
+ .accept()
+ .expect("accept")
+ .await
+ .expect("connecting");
+ let router = DatagramRouter::spawn(conn);
+ let mut rx = router.register(7);
+ if let Some((sub, payload)) = rx.recv().await {
+ router.send(7, sub, &payload).expect("send reply");
+ }
+ tokio::time::sleep(Duration::from_millis(200)).await;
+ });
+
+ let addr =
+ EndpointAddr::from_parts(agent_id, [TransportAddr::Custom(ssh_custom_addr(agent_id))]);
+ let conn = client.connect(addr, REMOWT_ALPN).await.expect("connect");
+ let router = DatagramRouter::spawn(conn);
+ let mut rx = router.register(7);
+ router.send(7, 42, b"ping").expect("send");
+ let (sub, payload) = tokio::time::timeout(Duration::from_secs(5), rx.recv())
+ .await
+ .expect("datagram timed out")
+ .expect("router closed");
+ assert_eq!(sub, 42);
+ assert_eq!(&payload[..], b"ping");
+ let _ = server.await;
+ }
+}
remowt/crates/remowt-link-shared/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-link-shared/src/lib.rs
@@ -0,0 +1,89 @@
+use bifrostlink::error::{ErrorT, ListenerForYourRequestHasBeenDeadError, ResponseError};
+use bifrostlink::notification;
+use bifrostlink::packet::OpaquePacketWrapper;
+use bifrostlink::AddressT;
+use bytes::Bytes;
+use serde::de::DeserializeOwned;
+use serde::{Deserialize, Serialize};
+
+pub mod editor;
+pub mod iroh_tunnel;
+pub mod port;
+
+#[derive(Clone, Serialize, Hash, Eq, Debug, PartialEq, Deserialize)]
+pub enum Address {
+ User,
+ Agent,
+ AgentPrivileged,
+ Plugin(u16),
+}
+impl AddressT for Address {}
+
+pub mod plugin;
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error("listener is dead")]
+ ListenerDead,
+ #[error("response: {0}")]
+ Response(String),
+
+ #[error(transparent)]
+ Ui(#[from] remowt_ui_prompt::Error),
+}
+
+impl From<ListenerForYourRequestHasBeenDeadError> for Error {
+ fn from(_value: ListenerForYourRequestHasBeenDeadError) -> Self {
+ Self::ListenerDead
+ }
+}
+impl From<serde_json::Error> for Error {
+ fn from(_value: serde_json::Error) -> Self {
+ Self::ListenerDead
+ }
+}
+impl From<Error> for ResponseError {
+ fn from(val: Error) -> Self {
+ ResponseError(val.to_string())
+ }
+}
+impl From<ResponseError> for Error {
+ fn from(value: ResponseError) -> Self {
+ Self::Response(value.0)
+ }
+}
+impl ErrorT for Error {}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct TestNotification {
+ pub value: u32,
+}
+notification!((0x0100) TestNotification);
+
+pub struct BifConfig;
+impl bifrostlink::Config for BifConfig {
+ type Address = Address;
+
+ type Error = Error;
+
+ type EncodedData = Vec<u8>;
+
+ fn decode_headers(
+ data_with_headers: Bytes,
+ ) -> Result<(OpaquePacketWrapper<Self::Address>, Self::EncodedData), Self::Error> {
+ let (header, data): (OpaquePacketWrapper<Self::Address>, Vec<u8>) =
+ serde_json::from_slice(&data_with_headers)?;
+ Ok((header, data))
+ }
+
+ fn decode_data<T: DeserializeOwned>(data: Self::EncodedData) -> Result<T, Self::Error> {
+ let v: T = serde_json::from_slice(&data)?;
+ Ok(v)
+ }
+
+ fn encode_data<T: Serialize>(headers: OpaquePacketWrapper<Self::Address>, data: T) -> Bytes {
+ let data = serde_json::to_vec(&data).expect("serialization shouldn't fail");
+ let o = serde_json::to_vec(&(headers, data)).expect("serialization shouldn't fail");
+ o.into()
+ }
+}
remowt/crates/remowt-link-shared/src/plugin.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-link-shared/src/plugin.rs
@@ -0,0 +1,39 @@
+use std::future::Future;
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("plugin name must be a bare file name")]
+ BadName,
+ #[error("spawning plugin failed: {0}")]
+ Spawn(String),
+ #[error("agent is shutting down")]
+ Gone,
+}
+
+pub trait PluginHost: Send + Sync {
+ fn load_plugin(&self, id: u16, name: String) -> impl Future<Output = Result<(), Error>> + Send;
+
+ fn load_plugin_path(
+ &self,
+ id: u16,
+ path: String,
+ ) -> impl Future<Output = Result<(), Error>> + Send;
+}
+
+pub struct PluginEndpoints<H>(pub H);
+
+#[endpoints(ns = 9)]
+impl<H: PluginHost + 'static> PluginEndpoints<H> {
+ #[endpoints(id = 1)]
+ async fn load_plugin(&self, id: u16, name: String) -> Result<(), Error> {
+ self.0.load_plugin(id, name).await
+ }
+ #[endpoints(id = 2)]
+ async fn load_plugin_path(&self, id: u16, path: String) -> Result<(), Error> {
+ self.0.load_plugin_path(id, path).await
+ }
+}
remowt/crates/remowt-link-shared/src/port.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-link-shared/src/port.rs
@@ -0,0 +1,82 @@
+use std::io;
+
+use bifrostlink::Port;
+use bytes::{Bytes, BytesMut};
+use tokio::io::{AsyncRead, AsyncReadExt as _, AsyncWrite, AsyncWriteExt as _};
+use tokio::select;
+
+pub fn child_port<R, W>(mut reader: R, mut writer: W) -> Port
+where
+ R: AsyncRead + Unpin + Send + 'static,
+ W: AsyncWrite + Unpin + Send + 'static,
+{
+ Port::new(|mut rx, tx| async move {
+ let read_task = async move {
+ loop {
+ let len = match reader.read_u32().await {
+ Ok(len) => len,
+ Err(e) => {
+ log_read_end(&e);
+ break;
+ }
+ };
+ let mut buf = BytesMut::zeroed(len as usize);
+ if let Err(e) = reader.read_exact(&mut buf).await {
+ log_read_end(&e);
+ break;
+ }
+ if tx.send(buf.freeze()).is_err() {
+ break;
+ }
+ }
+ };
+ let write_task = async move {
+ while let Some(msg) = rx.recv().await {
+ if let Err(e) = write_frame(&mut writer, msg).await {
+ log_write_end(&e);
+ break;
+ }
+ }
+ };
+ select! {
+ _ = read_task => {},
+ _ = write_task => {},
+ }
+ })
+}
+
+fn log_read_end(e: &io::Error) {
+ if matches!(
+ e.kind(),
+ io::ErrorKind::UnexpectedEof
+ | io::ErrorKind::BrokenPipe
+ | io::ErrorKind::ConnectionReset
+ | io::ErrorKind::ConnectionAborted
+ ) {
+ tracing::debug!("child read ended: {e}");
+ } else {
+ tracing::error!("child read failed: {e}");
+ }
+}
+
+fn log_write_end(e: &io::Error) {
+ if matches!(
+ e.kind(),
+ io::ErrorKind::BrokenPipe
+ | io::ErrorKind::ConnectionReset
+ | io::ErrorKind::ConnectionAborted
+ ) {
+ tracing::debug!("child write ended: {e}");
+ } else {
+ tracing::error!("child write failed: {e}");
+ }
+}
+
+async fn write_frame<W: AsyncWrite + Unpin>(writer: &mut W, msg: Bytes) -> io::Result<()> {
+ let len = u32::try_from(msg.len())
+ .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "message larger than 4GB"))?;
+ writer.write_u32(len).await?;
+ writer.write_all(&msg).await?;
+ writer.flush().await?;
+ Ok(())
+}
remowt/crates/remowt-plugin/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-plugin/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "remowt-plugin"
+description = "Plugin host and protocol for remowt agents"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+bifrostlink.workspace = true
+bifrostlink-ports.workspace = true
+remowt-link-shared.workspace = true
+serde_json.workspace = true
+tokio = { workspace = true, features = [
+ "rt",
+ "net",
+ "io-std",
+ "io-util",
+ "macros",
+ "time",
+ "process",
+] }
+tracing-subscriber.workspace = true
remowt/crates/remowt-plugin/src/host.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-plugin/src/host.rs
@@ -0,0 +1,70 @@
+use std::ffi::OsStr;
+use std::process::Stdio;
+use std::sync::Mutex;
+
+use bifrostlink::{Rpc, Rtt, WeakRpc};
+use tokio::process::{Child, Command};
+
+use remowt_link_shared::plugin::{Error, PluginEndpoints, PluginHost};
+use remowt_link_shared::port::child_port;
+use remowt_link_shared::{Address, BifConfig};
+
+pub fn serve(rpc: &mut Rpc<BifConfig>) {
+ let host = Host {
+ me: rpc.me(),
+ rpc: rpc.clone().downgrade(),
+ children: Mutex::new(Vec::new()),
+ };
+ PluginEndpoints(host).register_endpoints(rpc);
+}
+
+struct Host {
+ me: Address,
+ rpc: WeakRpc<BifConfig>,
+ children: Mutex<Vec<Child>>,
+}
+
+impl Host {
+ fn spawn(&self, id: u16, path: impl AsRef<OsStr>) -> Result<(), Error> {
+ let rpc = self.rpc.clone().upgrade().ok_or(Error::Gone)?;
+
+ let mut child = Command::new(path)
+ .arg(id.to_string())
+ .arg(serde_json::to_string(&self.me).expect("address serializes"))
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .kill_on_drop(true)
+ .spawn()
+ .map_err(|e| Error::Spawn(e.to_string()))?;
+ let stdin = child.stdin.take().expect("stdin piped");
+ let stdout = child.stdout.take().expect("stdout piped");
+
+ let addr = Address::Plugin(id);
+ rpc.add_direct(addr, child_port(stdout, stdin), Rtt(0));
+ self.children.lock().expect("not poisoned").push(child);
+ Ok(())
+ }
+}
+
+impl PluginHost for Host {
+ async fn load_plugin(&self, id: u16, name: String) -> Result<(), Error> {
+ // TODO: Right now loads plugin next to the binary...
+ // But with our CA addressed schema, the plugins should be located in content-addressed subdir...
+ // Maybe it should just be scrapped in favor of load_plugin_path.
+ if name.is_empty() || name == "." || name == ".." || name.contains(['/', '\0']) {
+ return Err(Error::BadName);
+ }
+ let exe = std::env::current_exe().map_err(|e| Error::Spawn(e.to_string()))?;
+ let dir = exe
+ .parent()
+ .ok_or_else(|| Error::Spawn("primary agent has no parent directory".to_owned()))?;
+ self.spawn(id, dir.join(&name))
+ }
+
+ async fn load_plugin_path(&self, id: u16, path: String) -> Result<(), Error> {
+ if path.is_empty() || path.contains('\0') {
+ return Err(Error::BadName);
+ }
+ self.spawn(id, path)
+ }
+}
remowt/crates/remowt-plugin/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-plugin/src/lib.rs
@@ -0,0 +1,46 @@
+use std::future::pending;
+
+use anyhow::Result;
+use bifrostlink::{Rpc, Rtt};
+use bifrostlink_ports::stdio::from_stdio;
+use tokio::runtime::Builder;
+
+pub mod host;
+
+pub use bifrostlink;
+pub use remowt_link_shared::{self, Address, BifConfig};
+
+pub fn plugin_index() -> Result<u16> {
+ let arg = std::env::args()
+ .nth(1)
+ .ok_or_else(|| anyhow::anyhow!("missing plugin index argument"))?;
+ arg.parse()
+ .map_err(|e| anyhow::anyhow!("invalid plugin index {arg:?}: {e}"))
+}
+
+pub fn host_address() -> Result<Address> {
+ let arg = std::env::args()
+ .nth(2)
+ .ok_or_else(|| anyhow::anyhow!("missing host address argument"))?;
+ serde_json::from_str(&arg).map_err(|e| anyhow::anyhow!("invalid host address {arg:?}: {e}"))
+}
+
+pub fn run<F>(register: F) -> Result<()>
+where
+ F: FnOnce(&mut Rpc<BifConfig>),
+{
+ tracing_subscriber::fmt()
+ .with_writer(std::io::stderr)
+ .init();
+
+ let index = plugin_index()?;
+ let host = host_address()?;
+ let runtime = Builder::new_current_thread().enable_all().build()?;
+ runtime.block_on(async move {
+ let mut rpc = Rpc::<BifConfig>::new(Address::Plugin(index));
+ rpc.add_direct(host, from_stdio(), Rtt(0));
+ register(&mut rpc);
+ let _rpc = rpc;
+ pending::<Result<()>>().await
+ })
+}
remowt/crates/remowt-ui-prompt/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-ui-prompt/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "remowt-ui-prompt"
+description = "Interactive UI prompt endpoint for remowt (D-Bus)"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+bifrostlink.workspace = true
+bifrostlink-macros.workspace = true
+serde.workspace = true
+thiserror.workspace = true
+tokio = { workspace = true, features = ["io-util", "macros", "process", "rt"] }
+tracing.workspace = true
+zbus = { workspace = true, optional = true }
+
+[features]
+default = ["dbus"]
+dbus = ["dep:zbus"]
remowt/crates/remowt-ui-prompt/src/auto.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-ui-prompt/src/auto.rs
@@ -0,0 +1,83 @@
+use anyhow::bail;
+use tracing::debug;
+use zbus::fdo::DBusProxy;
+use zbus::names::BusName;
+
+use crate::dbus::{DbusPrompterProxy, BUS_NAME, PROMPTER_PATH};
+use crate::rofi::RofiPrompter;
+use crate::{Prompter, Result, Source};
+
+pub struct AutoPrompter {
+ dbus: Option<DbusPrompterProxy<'static>>,
+ fallback: RofiPrompter,
+}
+
+impl AutoPrompter {
+ pub async fn new() -> Self {
+ let dbus = match Self::try_dbus().await {
+ Ok(p) => Some(p),
+ Err(e) => {
+ debug!("dbus prompter unavailable, falling back to rofi: {e}");
+ None
+ }
+ };
+ Self {
+ dbus,
+ fallback: RofiPrompter,
+ }
+ }
+
+ async fn try_dbus() -> anyhow::Result<DbusPrompterProxy<'static>> {
+ let conn = zbus::Connection::session().await?;
+ let dbus = DBusProxy::new(&conn).await?;
+ let name = BusName::try_from(BUS_NAME)?;
+ if !dbus.name_has_owner(name).await? {
+ bail!("{BUS_NAME} not registered on session bus");
+ }
+ let proxy = DbusPrompterProxy::builder(&conn)
+ .destination(BUS_NAME)?
+ .path(PROMPTER_PATH)?
+ .build()
+ .await?;
+ Ok(proxy)
+ }
+}
+
+impl Prompter for AutoPrompter {
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ if let Some(dbus) = &self.dbus {
+ return Prompter::prompt_enum(dbus, prompt, description, variants, source).await;
+ }
+ self.fallback
+ .prompt_enum(prompt, description, variants, source)
+ .await
+ }
+
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ if let Some(dbus) = &self.dbus {
+ return Prompter::prompt_text(dbus, echo, prompt, description, source).await;
+ }
+ self.fallback
+ .prompt_text(echo, prompt, description, source)
+ .await
+ }
+
+ async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ if let Some(dbus) = &self.dbus {
+ return Prompter::display_text(dbus, error, description, source).await;
+ }
+ self.fallback.display_text(error, description, source).await
+ }
+}
remowt/crates/remowt-ui-prompt/src/bifrost.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-ui-prompt/src/bifrost.rs
@@ -0,0 +1,109 @@
+use bifrostlink::{Config, Rpc};
+use bifrostlink_macros::endpoints;
+use serde::{Deserialize, Serialize};
+
+use crate::{Error, Prompter, Source};
+
+pub struct PromptEndpoints<P>(pub P);
+
+#[endpoints(ns = 2)]
+impl<P> PromptEndpoints<P>
+where
+ P: Prompter + Send + Sync + 'static,
+{
+ #[endpoints(id = 1, cancel)]
+ async fn prompt_enum(
+ &self,
+ prompt: String,
+ description: String,
+ variants: Vec<String>,
+ source: Vec<Source>,
+ ) -> Result<u32, Error> {
+ let variants: Vec<&str> = variants.iter().map(|v| v.as_str()).collect();
+ self.0
+ .prompt_enum(&prompt, &description, &variants, &source)
+ .await
+ }
+
+ #[endpoints(id = 2, cancel)]
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: String,
+ description: String,
+ source: Vec<Source>,
+ ) -> Result<String, Error> {
+ self.0
+ .prompt_text(echo, &prompt, &description, &source)
+ .await
+ }
+
+ #[endpoints(id = 3, cancel)]
+ async fn display_text(
+ &self,
+ error: bool,
+ description: String,
+ source: Vec<Source>,
+ ) -> Result<(), Error> {
+ self.0.display_text(error, &description, &source).await
+ }
+}
+
+impl<C: Config> Prompter for PromptEndpointsClient<C>
+where
+ Error: ToString,
+{
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> crate::Result<u32> {
+ self.prompt_enum(
+ prompt.to_owned(),
+ description.to_owned(),
+ variants.iter().map(|v| (*v).to_owned()).collect(),
+ source.to_vec(),
+ )
+ .await
+ .map_err(|e| Error::Remote(e.to_string()))?
+ }
+
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> crate::Result<String> {
+ self.prompt_text(
+ echo,
+ prompt.to_owned(),
+ description.to_owned(),
+ source.to_vec(),
+ )
+ .await
+ .map_err(|e| Error::Remote(e.to_string()))?
+ }
+
+ async fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: &[Source],
+ ) -> crate::Result<()> {
+ self.display_text(error, description.to_owned(), source.to_vec())
+ .await
+ .map_err(|e| Error::Remote(e.to_string()))?
+ }
+}
+
+pub fn serve_prompts<P, C>(rpc: &mut Rpc<C>, prompt: P)
+where
+ P: Prompter + Send + Sync + 'static,
+ C: Config,
+ C::Error: From<Error>,
+{
+ PromptEndpoints(prompt).register_endpoints(rpc);
+}
remowt/crates/remowt-ui-prompt/src/dbus.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-ui-prompt/src/dbus.rs
@@ -0,0 +1,143 @@
+use zbus::interface;
+use zbus::{fdo, proxy};
+
+use crate::Source;
+use crate::{BlockingPrompter, Result};
+use crate::{Error, Prompter};
+
+pub const BUS_NAME: &str = "lach.RemowtAskpass";
+pub const PROMPTER_PATH: &str = "/lach/Askpass";
+
+pub struct DbusPrompterInterface<P>(pub P);
+
+#[interface(name = "lach.PolkitInputHandler")]
+impl<P: Prompter + Send + Sync + 'static> DbusPrompterInterface<P> {
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: Vec<String>,
+ source: Vec<Source>,
+ ) -> fdo::Result<u32> {
+ let variants: Vec<&str> = variants.iter().map(|v| v.as_str()).collect();
+ Ok(self
+ .0
+ .prompt_enum(prompt, description, &variants, &source)
+ .await?)
+ }
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: Vec<Source>,
+ ) -> fdo::Result<String> {
+ Ok(self
+ .0
+ .prompt_text(echo, prompt, description, &source)
+ .await?)
+ }
+ async fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: Vec<Source>,
+ ) -> fdo::Result<()> {
+ Ok(self.0.display_text(error, description, &source).await?)
+ }
+}
+
+#[proxy(interface = "lach.PolkitInputHandler")]
+pub trait DbusPrompter {
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> fdo::Result<u32>;
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> fdo::Result<String>;
+ async fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: &[Source],
+ ) -> fdo::Result<()>;
+}
+
+impl Prompter for DbusPrompterProxy<'_> {
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ Ok(self
+ .prompt_enum(prompt, description, variants, source)
+ .await?)
+ }
+
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ Ok(self.prompt_text(echo, prompt, description, source).await?)
+ }
+
+ async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ Ok(self.display_text(error, description, source).await?)
+ }
+}
+impl BlockingPrompter for DbusPrompterProxyBlocking<'_> {
+ fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ Ok(self.prompt_enum(prompt, description, variants, source)?)
+ }
+
+ fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ Ok(self.prompt_text(echo, prompt, description, source)?)
+ }
+
+ fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ Ok(self.display_text(error, description, source)?)
+ }
+}
+
+impl From<fdo::Error> for Error {
+ fn from(value: fdo::Error) -> Self {
+ if matches!(value, fdo::Error::NoReply(_)) {
+ return Self::Cancel;
+ }
+ Self::InputError(format!("{value}"))
+ }
+}
+impl From<Error> for fdo::Error {
+ fn from(value: Error) -> Self {
+ match value {
+ Error::Cancel => fdo::Error::NoReply("input was cancelled".to_owned()),
+ Error::Remote(e) => fdo::Error::NoReply(format!("remote error occured: {e}")),
+ Error::InputError(e) => fdo::Error::Failed(e),
+ }
+ }
+}
remowt/crates/remowt-ui-prompt/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-ui-prompt/src/lib.rs
@@ -0,0 +1,202 @@
+use core::fmt;
+use std::borrow::Cow;
+use std::future::Future;
+use std::result;
+
+pub mod auto;
+pub mod bifrost;
+pub mod dbus;
+pub mod rofi;
+
+#[derive(thiserror::Error, Debug, serde::Serialize, serde::Deserialize)]
+pub enum Error {
+ #[error("user has cancelled input")]
+ Cancel,
+ #[error("input error: {0}")]
+ InputError(String),
+ #[error("unknown remote error: {0}")]
+ Remote(String),
+}
+
+pub type Result<T, E = Error> = result::Result<T, E>;
+
+#[cfg_attr(feature = "dbus", derive(zbus::zvariant::Type))]
+#[derive(serde::Serialize, serde::Deserialize, Clone)]
+pub struct Source(pub Cow<'static, str>);
+impl fmt::Display for Source {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "<u>{}</u>", self.0)
+ }
+}
+
+pub trait Prompter: Send + Sync {
+ fn prompt_radio(
+ &self,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<bool>> + Send {
+ let fut = self.prompt_enum(prompt, description, &["No", "Yes"], source);
+ async { fut.await.map(|v| v == 1) }
+ }
+ fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> impl Future<Output = Result<u32>> + Send;
+ fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<String>> + Send;
+ fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<()>> + Send;
+}
+pub trait BlockingPrompter {
+ fn prompt_radio(&self, prompt: &str, description: &str, source: &[Source]) -> Result<bool> {
+ self.prompt_enum(prompt, description, &["No", "Yes"], source)
+ .map(|v| v == 1)
+ }
+ fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32>;
+ fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String>;
+ fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()>;
+}
+impl<P> Prompter for &P
+where
+ P: Prompter,
+{
+ fn prompt_radio(
+ &self,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<bool>> + Send {
+ (*self).prompt_radio(prompt, description, source)
+ }
+
+ fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> impl Future<Output = Result<u32>> + Send {
+ (*self).prompt_enum(prompt, description, variants, source)
+ }
+
+ fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<String>> + Send {
+ (*self).prompt_text(echo, prompt, description, source)
+ }
+
+ fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<()>> + Send {
+ (*self).display_text(error, description, source)
+ }
+}
+
+pub struct PrependSourcePrompter<P> {
+ pub prompter: P,
+ pub source: Vec<Source>,
+ pub description: String,
+}
+impl<P> PrependSourcePrompter<P> {
+ fn source(&self, input: &[Source]) -> Vec<Source> {
+ let mut out = self.source.clone();
+ out.extend(input.iter().cloned());
+ out
+ }
+ fn description(&self, input: &str) -> String {
+ if self.description.is_empty() {
+ input.to_owned()
+ } else if input.is_empty() {
+ self.description.to_owned()
+ } else {
+ format!("{input}\n\n{}", self.description)
+ }
+ }
+}
+impl<P> Prompter for PrependSourcePrompter<P>
+where
+ P: Prompter + Sync,
+{
+ async fn prompt_radio(
+ &self,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<bool> {
+ self.prompter
+ .prompt_radio(prompt, &self.description(description), &self.source(source))
+ .await
+ }
+
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ self.prompter
+ .prompt_enum(
+ prompt,
+ &self.description(description),
+ variants,
+ &self.source(source),
+ )
+ .await
+ }
+
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ self.prompter
+ .prompt_text(
+ echo,
+ prompt,
+ &self.description(description),
+ &self.source(source),
+ )
+ .await
+ }
+
+ async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ self.prompter
+ .display_text(error, &self.description(description), &self.source(source))
+ .await
+ }
+}
remowt/crates/remowt-ui-prompt/src/rofi.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-ui-prompt/src/rofi.rs
@@ -0,0 +1,226 @@
+use std::process::Stdio;
+
+use tokio::io::AsyncWriteExt;
+use tokio::process::Command;
+use tracing::trace;
+
+use crate::{Error, Prompter, Result, Source};
+
+#[derive(Clone)]
+pub struct RofiPrompter;
+
+fn fixup_prompt(prompt: &str) -> &str {
+ // Rofi always appends such suffix
+ prompt.strip_suffix(": ").unwrap_or(prompt)
+}
+
+fn rofi_command() -> Command {
+ Command::new(option_env!("ROFI").unwrap_or("rofi"))
+}
+
+impl Prompter for RofiPrompter {
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ trace!("rofi radio");
+ let mut cmd = rofi_command();
+ let mesg = if source.is_empty() {
+ description.to_owned()
+ } else {
+ let mut out = format!("{description}\n\n<b>Requested on ",);
+ for (i, s) in source.iter().enumerate() {
+ if i != 0 {
+ out.push_str(" -> ");
+ }
+ out.push_str(&s.to_string());
+ }
+ out.push_str("</b>");
+ out
+ };
+ cmd.args([
+ "-dmenu",
+ "-mesg",
+ &mesg,
+ "-sync",
+ "-no-custom",
+ "-p",
+ fixup_prompt(prompt),
+ "-format",
+ "i",
+ "-markup-rows",
+ ]);
+ cmd.stdin(Stdio::piped());
+ cmd.stdout(Stdio::piped());
+ cmd.kill_on_drop(true);
+ let mut child = cmd
+ .spawn()
+ .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
+
+ let mut stdin = child.stdin.take().expect("stdin is piped");
+ for var in variants {
+ stdin
+ .write_all(var.replace('\n', " ").as_bytes())
+ .await
+ .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;
+ stdin
+ .write_all(b"\n")
+ .await
+ .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;
+ }
+ // write_all already flushes, just to be sure.
+ let _ = stdin.flush().await;
+ drop(stdin);
+
+ let out = child
+ .wait_with_output()
+ .await
+ .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
+ match out.status.code() {
+ Some(0) => {}
+ Some(1) => return Err(Error::Cancel),
+ other => {
+ return Err(Error::InputError(format!(
+ "rofi exited with status {other:?}"
+ )));
+ }
+ }
+ let stdout = out
+ .stdout
+ .strip_suffix(b"\n")
+ .unwrap_or(&out.stdout)
+ .to_owned();
+
+ let id: u32 = String::from_utf8(stdout)
+ .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?
+ .parse()
+ .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?;
+ if id as usize >= variants.len() {
+ return Err(Error::InputError("invalid rofi response".to_owned()));
+ }
+
+ Ok(id)
+ }
+
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ trace!("rofi text");
+ let mut cmd = rofi_command();
+ let mesg = if source.is_empty() {
+ description.to_owned()
+ } else {
+ let mut out = format!("{description}\n\n<b>Requested on ",);
+ for (i, s) in source.iter().enumerate() {
+ if i != 0 {
+ out.push_str(" -> ");
+ }
+ out.push_str(&s.to_string());
+ }
+ out.push_str("</b>");
+ out
+ };
+ cmd.args(["-dmenu", "-mesg", &mesg, "-p", fixup_prompt(prompt)]);
+ if !echo {
+ cmd.arg("-password");
+ }
+ cmd.stdin(Stdio::null());
+ cmd.stdout(Stdio::piped());
+ cmd.kill_on_drop(true);
+ let child = cmd
+ .spawn()
+ .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
+
+ let out = child
+ .wait_with_output()
+ .await
+ .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
+ match out.status.code() {
+ Some(0) => {}
+ Some(1) => return Err(Error::Cancel),
+ other => {
+ return Err(Error::InputError(format!(
+ "rofi exited with status {other:?}"
+ )));
+ }
+ }
+ let stdout = out
+ .stdout
+ .strip_suffix(b"\n")
+ .unwrap_or(&out.stdout)
+ .to_owned();
+
+ Ok(String::from_utf8_lossy(&stdout).to_string())
+ }
+
+ async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ trace!("rofi display");
+ let mut cmd = rofi_command();
+ let mut mesg = if source.is_empty() {
+ description.to_owned()
+ } else {
+ let mut out = format!("{description}\n\n<b>Coming from ",);
+ for s in source.iter() {
+ out.push_str(&s.to_string());
+ }
+ out.push_str("</b>");
+ out
+ };
+ if error {
+ mesg.insert_str(0, "<span color=\"red\">");
+ mesg.push_str("</span>");
+ }
+ cmd.args(["-e", &mesg, "-markup"]);
+ cmd.stdin(Stdio::null());
+ cmd.stdout(Stdio::null());
+ cmd.kill_on_drop(true);
+ let mut child = cmd
+ .spawn()
+ .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
+
+ child
+ .wait()
+ .await
+ .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
+
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::borrow::Cow;
+
+ use crate::rofi::RofiPrompter;
+ use crate::{PrependSourcePrompter, Prompter as _, Source};
+
+ // #[tokio::test]
+ #[tokio::test]
+ #[ignore = "interactive"]
+ async fn test() {
+ let prompter = PrependSourcePrompter {
+ prompter: RofiPrompter,
+ description: "test".to_owned(),
+ source: vec![Source(Cow::Borrowed("ssh"))],
+ };
+ prompter
+ .prompt_radio("Enable", "Polkit needs access", &[])
+ .await
+ .expect("rofi");
+ prompter
+ .prompt_text(false, "Password", "Polkit needs access", &[])
+ .await
+ .expect("rofi");
+ prompter
+ .display_text(true, "Polkit needs access", &[])
+ .await
+ .expect("rofi");
+ }
+}