git.delta.rocks / remowt / refs/commits / 705d3cdac982

difftreelog

feat initial work on russh+remowt prototype

mvluospuYaroslav Bolyukin2024-08-24parent: #5cb6be4.patch.diff
in: trunk

30 files changed

added.rustfmt.tomldiffbeforeafterboth
--- /dev/null
+++ b/.rustfmt.toml
@@ -0,0 +1 @@
+hard_tabs = true
modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -18,6 +18,47 @@
 checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
 [[package]]
+name = "adler2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+
+[[package]]
+name = "aead"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
+dependencies = [
+ "crypto-common",
+ "generic-array",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aes-gcm"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
+dependencies = [
+ "aead",
+ "aes",
+ "cipher",
+ "ctr",
+ "ghash",
+ "subtle",
+]
+
+[[package]]
 name = "aho-corasick"
 version = "1.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -247,12 +288,60 @@
  "cc",
  "cfg-if",
  "libc",
- "miniz_oxide",
+ "miniz_oxide 0.7.4",
  "object",
  "rustc-demangle",
 ]
 
 [[package]]
+name = "base16ct"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
+
+[[package]]
+name = "base64ct"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+
+[[package]]
+name = "bcrypt-pbkdf"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2"
+dependencies = [
+ "blowfish",
+ "pbkdf2 0.12.2",
+ "sha2",
+]
+
+[[package]]
+name = "bifrostlink"
+version = "0.1.0"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "derivative",
+ "futures",
+ "serde",
+ "serde_json",
+ "tokio",
+ "tracing",
+ "uuid",
+]
+
+[[package]]
+name = "bifrostlink-ports"
+version = "0.1.0"
+dependencies = [
+ "bifrostlink",
+ "bytes",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
 name = "bindgen"
 version = "0.69.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -294,6 +383,15 @@
 ]
 
 [[package]]
+name = "block-padding"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
 name = "blocking"
 version = "1.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -307,6 +405,32 @@
 ]
 
 [[package]]
+name = "blowfish"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
+dependencies = [
+ "byteorder",
+ "cipher",
+]
+
+[[package]]
+name = "bstr"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+
+[[package]]
 name = "byteorder"
 version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -319,6 +443,15 @@
 checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
 
 [[package]]
+name = "cbc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
 name = "cc"
 version = "1.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -346,6 +479,27 @@
 checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
 
 [[package]]
+name = "chacha20"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
 name = "clang-sys"
 version = "1.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -357,9 +511,9 @@
 
 [[package]]
 name = "clap"
-version = "4.5.13"
+version = "4.5.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc"
+checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -367,9 +521,9 @@
 
 [[package]]
 name = "clap_builder"
-version = "4.5.13"
+version = "4.5.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99"
+checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
 dependencies = [
  "anstream",
  "anstyle",
@@ -411,6 +565,12 @@
 ]
 
 [[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
 name = "cpufeatures"
 version = "0.2.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -420,38 +580,206 @@
 ]
 
 [[package]]
+name = "crc32fast"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
 name = "crossbeam-utils"
 version = "0.8.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
 
 [[package]]
+name = "crypto-bigint"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+dependencies = [
+ "generic-array",
+ "rand_core 0.6.4",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
 name = "crypto-common"
 version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
 dependencies = [
  "generic-array",
+ "rand_core 0.6.4",
  "typenum",
 ]
 
 [[package]]
+name = "ctr"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "curve25519-dalek-derive",
+ "digest",
+ "fiat-crypto",
+ "rustc_version",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.72",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
+
+[[package]]
+name = "delegate"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e018fccbeeb50ff26562ece792ed06659b9c2dae79ece77c4456bb10d9bf79b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.72",
+]
+
+[[package]]
+name = "der"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
+dependencies = [
+ "const-oid",
+ "pem-rfc7468",
+ "zeroize",
+]
+
+[[package]]
+name = "derivative"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "des"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
 name = "digest"
 version = "0.10.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
 dependencies = [
  "block-buffer",
+ "const-oid",
  "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "ecdsa"
+version = "0.16.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
+dependencies = [
+ "der",
+ "digest",
+ "elliptic-curve",
+ "rfc6979",
+ "signature",
+ "spki",
+]
+
+[[package]]
+name = "ed25519"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
+dependencies = [
+ "pkcs8",
+ "signature",
 ]
 
 [[package]]
+name = "ed25519-dalek"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "rand_core 0.6.4",
+ "serde",
+ "sha2",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
 name = "either"
 version = "1.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
 
 [[package]]
+name = "elliptic-curve"
+version = "0.13.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
+dependencies = [
+ "base16ct",
+ "crypto-bigint",
+ "digest",
+ "ff",
+ "generic-array",
+ "group",
+ "hkdf",
+ "pem-rfc7468",
+ "pkcs8",
+ "rand_core 0.6.4",
+ "sec1",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
 name = "endi"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -533,12 +861,80 @@
 checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
 
 [[package]]
+name = "ff"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
+dependencies = [
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
+[[package]]
+name = "flate2"
+version = "1.0.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c0596c1eac1f9e04ed902702e9878208b336edc9d6fddc8a48387349bab3666"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide 0.8.0",
+]
+
+[[package]]
+name = "fuchsia-cprng"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
+
+[[package]]
+name = "futures"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
 name = "futures-core"
 version = "0.3.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
 
 [[package]]
+name = "futures-executor"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
 name = "futures-io"
 version = "0.3.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -558,6 +954,17 @@
 ]
 
 [[package]]
+name = "futures-macro"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.72",
+]
+
+[[package]]
 name = "futures-sink"
 version = "0.3.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -575,8 +982,10 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
 dependencies = [
+ "futures-channel",
  "futures-core",
  "futures-io",
+ "futures-macro",
  "futures-sink",
  "futures-task",
  "memchr",
@@ -593,6 +1002,7 @@
 dependencies = [
  "typenum",
  "version_check",
+ "zeroize",
 ]
 
 [[package]]
@@ -607,6 +1017,16 @@
 ]
 
 [[package]]
+name = "ghash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
+[[package]]
 name = "gimli"
 version = "0.29.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -619,6 +1039,30 @@
 checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
 
 [[package]]
+name = "globset"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "log",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "group"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
+dependencies = [
+ "ff",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
 name = "hashbrown"
 version = "0.14.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -649,6 +1093,39 @@
 checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
 
 [[package]]
+name = "hex-literal"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
+
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "home"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
 name = "indexmap"
 version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -659,6 +1136,16 @@
 ]
 
 [[package]]
+name = "inout"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+dependencies = [
+ "block-padding",
+ "generic-array",
+]
+
+[[package]]
 name = "is_terminal_polyfill"
 version = "1.70.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -680,10 +1167,22 @@
 checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
 
 [[package]]
+name = "js-sys"
+version = "0.3.70"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
 name = "lazy_static"
 version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+dependencies = [
+ "spin",
+]
 
 [[package]]
 name = "lazycell"
@@ -698,6 +1197,12 @@
 checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
 
 [[package]]
+name = "libm"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
+
+[[package]]
 name = "linux-raw-sys"
 version = "0.4.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -710,6 +1215,12 @@
 checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
 
 [[package]]
+name = "md5"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
+
+[[package]]
 name = "memchr"
 version = "2.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -740,6 +1251,15 @@
 ]
 
 [[package]]
+name = "miniz_oxide"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
 name = "mio"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -775,6 +1295,15 @@
 ]
 
 [[package]]
+name = "non-zero-byte-slice"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89daa1daa11c9df05d1181bcd0936d8066f8543144d77b09808eb78d65e38024"
+dependencies = [
+ "serde",
+]
+
+[[package]]
 name = "nu-ansi-term"
 version = "0.46.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -785,6 +1314,64 @@
 ]
 
 [[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
+dependencies = [
+ "byteorder",
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand 0.8.5",
+ "smallvec",
+ "zeroize",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+ "libm",
+]
+
+[[package]]
 name = "object"
 version = "0.36.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -800,6 +1387,55 @@
 checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
 
 [[package]]
+name = "opaque-debug"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+
+[[package]]
+name = "openssh"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f27389e5da64700a3efb7f925e442f824f6e3d4b1c27f75e115a92ad3aecbb1"
+dependencies = [
+ "libc",
+ "once_cell",
+ "openssh-mux-client",
+ "shell-escape",
+ "tempfile",
+ "thiserror",
+ "tokio",
+]
+
+[[package]]
+name = "openssh-mux-client"
+version = "0.17.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b87a1b6780afc5f9f38f81f7928c91c3c24532c48914477ab6caf2ba076ae866"
+dependencies = [
+ "cfg-if",
+ "non-zero-byte-slice",
+ "once_cell",
+ "openssh-mux-client-error",
+ "sendfd",
+ "serde",
+ "ssh_format",
+ "tokio",
+ "tokio-io-utility",
+ "typed-builder",
+]
+
+[[package]]
+name = "openssh-mux-client-error"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "015d49e592f4d2a456033e6ec48036588e8e58c8908424b1bc40994de58ae648"
+dependencies = [
+ "ssh_format_error",
+ "thiserror",
+]
+
+[[package]]
 name = "ordered-stream"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -816,6 +1452,58 @@
 checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
 
 [[package]]
+name = "p256"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
+dependencies = [
+ "ecdsa",
+ "elliptic-curve",
+ "primeorder",
+ "sha2",
+]
+
+[[package]]
+name = "p384"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209"
+dependencies = [
+ "ecdsa",
+ "elliptic-curve",
+ "primeorder",
+ "sha2",
+]
+
+[[package]]
+name = "p521"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2"
+dependencies = [
+ "base16ct",
+ "ecdsa",
+ "elliptic-curve",
+ "primeorder",
+ "rand_core 0.6.4",
+ "sha2",
+]
+
+[[package]]
+name = "pageant"
+version = "0.0.1-beta.3"
+source = "git+https://github.com/Eugeny/russh/#4115b8fd3a94c17c0178761ea769a40ca410903d"
+dependencies = [
+ "bytes",
+ "delegate",
+ "futures",
+ "rand 0.8.5",
+ "thiserror",
+ "tokio",
+ "windows",
+]
+
+[[package]]
 name = "pam-client"
 version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -846,6 +1534,48 @@
 checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
 
 [[package]]
+name = "password-hash"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
+dependencies = [
+ "base64ct",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "pbkdf2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
+dependencies = [
+ "digest",
+ "hmac",
+ "password-hash",
+ "sha2",
+]
+
+[[package]]
+name = "pbkdf2"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
+dependencies = [
+ "digest",
+ "hmac",
+]
+
+[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
+]
+
+[[package]]
 name = "pin-project-lite"
 version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -869,6 +1599,44 @@
 ]
 
 [[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
+
+[[package]]
+name = "pkcs5"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6"
+dependencies = [
+ "aes",
+ "cbc",
+ "der",
+ "pbkdf2 0.12.2",
+ "scrypt",
+ "sha2",
+ "spki",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "pkcs5",
+ "rand_core 0.6.4",
+ "spki",
+]
+
+[[package]]
 name = "polkit-backend"
 version = "0.1.0"
 dependencies = [
@@ -910,6 +1678,29 @@
 ]
 
 [[package]]
+name = "poly1305"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
+dependencies = [
+ "cpufeatures",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
+name = "polyval"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
 name = "ppv-lite86"
 version = "0.2.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -919,6 +1710,15 @@
 ]
 
 [[package]]
+name = "primeorder"
+version = "0.13.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
+dependencies = [
+ "elliptic-curve",
+]
+
+[[package]]
 name = "proc-macro-crate"
 version = "3.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -947,13 +1747,26 @@
 
 [[package]]
 name = "rand"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
+dependencies = [
+ "fuchsia-cprng",
+ "libc",
+ "rand_core 0.3.1",
+ "rdrand",
+ "winapi",
+]
+
+[[package]]
+name = "rand"
 version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
 dependencies = [
  "libc",
  "rand_chacha",
- "rand_core",
+ "rand_core 0.6.4",
 ]
 
 [[package]]
@@ -963,11 +1776,26 @@
 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
 dependencies = [
  "ppv-lite86",
- "rand_core",
+ "rand_core 0.6.4",
 ]
 
 [[package]]
 name = "rand_core"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
+dependencies = [
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
+
+[[package]]
+name = "rand_core"
 version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
@@ -976,6 +1804,24 @@
 ]
 
 [[package]]
+name = "rdrand"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
 name = "regex"
 version = "1.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1005,16 +1851,31 @@
 checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
 
 [[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
 name = "remowt-agent"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "bifrostlink",
+ "bifrostlink-ports",
  "clap",
- "pam-client",
+ "futures",
+ "futures-util",
+ "nix",
  "polkit-shared",
- "rand",
+ "rand 0.8.5",
+ "remowt-link-shared",
  "serde",
  "tokio",
+ "tokio-util",
  "tracing",
  "tracing-subscriber",
  "ui-prompt",
@@ -1024,10 +1885,47 @@
 ]
 
 [[package]]
+name = "remowt-link-shared"
+version = "0.1.0"
+dependencies = [
+ "bifrostlink",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tokio",
+]
+
+[[package]]
 name = "remowt-ssh"
 version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "bifrostlink",
+ "bifrostlink-ports",
+ "clap",
+ "openssh",
+ "remowt-link-shared",
+ "russh",
+ "russh-config",
+ "russh-keys",
+ "tempdir",
+ "tokio",
+ "tracing-subscriber",
+ "uuid",
+]
 
 [[package]]
+name = "rfc6979"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
+dependencies = [
+ "hmac",
+ "subtle",
+]
+
+[[package]]
 name = "rpassword"
 version = "6.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1040,6 +1938,143 @@
 ]
 
 [[package]]
+name = "rsa"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc"
+dependencies = [
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core 0.6.4",
+ "sha2",
+ "signature",
+ "spki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "russh"
+version = "0.46.0-beta.4"
+source = "git+https://github.com/Eugeny/russh/#4115b8fd3a94c17c0178761ea769a40ca410903d"
+dependencies = [
+ "aes",
+ "aes-gcm",
+ "async-trait",
+ "bitflags 2.6.0",
+ "byteorder",
+ "cbc",
+ "chacha20",
+ "ctr",
+ "curve25519-dalek",
+ "des",
+ "digest",
+ "elliptic-curve",
+ "flate2",
+ "futures",
+ "generic-array",
+ "hex-literal",
+ "hmac",
+ "log",
+ "num-bigint",
+ "once_cell",
+ "p256",
+ "p384",
+ "p521",
+ "poly1305",
+ "rand 0.8.5",
+ "rand_core 0.6.4",
+ "russh-cryptovec",
+ "russh-keys",
+ "sha1",
+ "sha2",
+ "ssh-encoding",
+ "ssh-key",
+ "subtle",
+ "thiserror",
+ "tokio",
+]
+
+[[package]]
+name = "russh-config"
+version = "0.7.1"
+source = "git+https://github.com/Eugeny/russh/#4115b8fd3a94c17c0178761ea769a40ca410903d"
+dependencies = [
+ "futures",
+ "globset",
+ "home",
+ "log",
+ "thiserror",
+ "tokio",
+ "whoami",
+]
+
+[[package]]
+name = "russh-cryptovec"
+version = "0.7.3"
+source = "git+https://github.com/Eugeny/russh/#4115b8fd3a94c17c0178761ea769a40ca410903d"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "russh-keys"
+version = "0.46.0-beta.3"
+source = "git+https://github.com/Eugeny/russh/#4115b8fd3a94c17c0178761ea769a40ca410903d"
+dependencies = [
+ "aes",
+ "async-trait",
+ "bcrypt-pbkdf",
+ "block-padding",
+ "byteorder",
+ "cbc",
+ "ctr",
+ "data-encoding",
+ "der",
+ "digest",
+ "ecdsa",
+ "ed25519-dalek",
+ "elliptic-curve",
+ "futures",
+ "hmac",
+ "home",
+ "inout",
+ "log",
+ "md5",
+ "num-integer",
+ "p256",
+ "p384",
+ "p521",
+ "pageant",
+ "pbkdf2 0.11.0",
+ "pkcs1",
+ "pkcs5",
+ "pkcs8",
+ "rand 0.8.5",
+ "rand_core 0.6.4",
+ "rsa",
+ "russh-cryptovec",
+ "sec1",
+ "serde",
+ "sha1",
+ "sha2",
+ "spki",
+ "ssh-encoding",
+ "ssh-key",
+ "thiserror",
+ "tokio",
+ "tokio-stream",
+ "typenum",
+ "zeroize",
+]
+
+[[package]]
 name = "rustc-demangle"
 version = "0.1.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1052,6 +2087,15 @@
 checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
 
 [[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
+[[package]]
 name = "rustix"
 version = "0.38.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1077,19 +2121,69 @@
 checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
 
 [[package]]
+name = "salsa20"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "scrypt"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
+dependencies = [
+ "pbkdf2 0.12.2",
+ "salsa20",
+ "sha2",
+]
+
+[[package]]
+name = "sec1"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
+dependencies = [
+ "base16ct",
+ "der",
+ "generic-array",
+ "pkcs8",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
+
+[[package]]
+name = "sendfd"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "604b71b8fc267e13bb3023a2c901126c8f349393666a6d98ac1ae5729b701798"
+dependencies = [
+ "libc",
+ "tokio",
+]
+
+[[package]]
 name = "serde"
-version = "1.0.204"
+version = "1.0.208"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
+checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.204"
+version = "1.0.208"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
+checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1098,9 +2192,9 @@
 
 [[package]]
 name = "serde_json"
-version = "1.0.122"
+version = "1.0.125"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da"
+checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
 dependencies = [
  "itoa",
  "memchr",
@@ -1131,6 +2225,17 @@
 ]
 
 [[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
 name = "sharded-slab"
 version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1140,6 +2245,12 @@
 ]
 
 [[package]]
+name = "shell-escape"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f"
+
+[[package]]
 name = "shlex"
 version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1155,6 +2266,16 @@
 ]
 
 [[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core 0.6.4",
+]
+
+[[package]]
 name = "slab"
 version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1180,6 +2301,92 @@
 ]
 
 [[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "ssh-cipher"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f"
+dependencies = [
+ "aes",
+ "aes-gcm",
+ "cbc",
+ "chacha20",
+ "cipher",
+ "ctr",
+ "poly1305",
+ "ssh-encoding",
+ "subtle",
+]
+
+[[package]]
+name = "ssh-encoding"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15"
+dependencies = [
+ "base64ct",
+ "pem-rfc7468",
+ "sha2",
+]
+
+[[package]]
+name = "ssh-key"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca9b366a80cf18bb6406f4cf4d10aebfb46140a8c0c33f666a144c5c76ecbafc"
+dependencies = [
+ "bcrypt-pbkdf",
+ "ed25519-dalek",
+ "num-bigint-dig",
+ "p256",
+ "p384",
+ "p521",
+ "rand_core 0.6.4",
+ "rsa",
+ "sec1",
+ "sha2",
+ "signature",
+ "ssh-cipher",
+ "ssh-encoding",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "ssh_format"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24ab31081d1c9097c327ec23550858cb5ffb4af6b866c1ef4d728455f01f3304"
+dependencies = [
+ "serde",
+ "ssh_format_error",
+]
+
+[[package]]
+name = "ssh_format_error"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be3c6519de7ca611f71ef7e8a56eb57aa1c818fecb5242d0a0f39c83776c210c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
 name = "static_assertions"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1192,6 +2399,12 @@
 checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
 
 [[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
 name = "syn"
 version = "1.0.109"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1214,6 +2427,16 @@
 ]
 
 [[package]]
+name = "tempdir"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
+dependencies = [
+ "rand 0.4.6",
+ "remove_dir_all",
+]
+
+[[package]]
 name = "tempfile"
 version = "3.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1258,9 +2481,9 @@
 
 [[package]]
 name = "tokio"
-version = "1.39.2"
+version = "1.39.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1"
+checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5"
 dependencies = [
  "backtrace",
  "bytes",
@@ -1275,6 +2498,15 @@
 ]
 
 [[package]]
+name = "tokio-io-utility"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d672654d175710e52c7c41f6aec77c62b3c0954e2a7ebce9049d1e94ed7c263"
+dependencies = [
+ "tokio",
+]
+
+[[package]]
 name = "tokio-macros"
 version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1286,6 +2518,31 @@
 ]
 
 [[package]]
+name = "tokio-stream"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
 name = "toml_datetime"
 version = "0.6.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1360,6 +2617,26 @@
 ]
 
 [[package]]
+name = "typed-builder"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e14ed59dc8b7b26cacb2a92bad2e8b1f098806063898ab42a3bd121d7d45e75"
+dependencies = [
+ "typed-builder-macro",
+]
+
+[[package]]
+name = "typed-builder-macro"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "560b82d656506509d43abe30e0ba64c56b1953ab3d4fe7ba5902747a7a3cedd5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.72",
+]
+
+[[package]]
 name = "typenum"
 version = "1.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1380,6 +2657,7 @@
 name = "ui-prompt"
 version = "0.1.0"
 dependencies = [
+ "bifrostlink",
  "serde",
  "thiserror",
  "tokio",
@@ -1394,6 +2672,16 @@
 checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
 
 [[package]]
+name = "universal-hash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
+dependencies = [
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
 name = "utf8parse"
 version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1427,6 +2715,88 @@
 checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
 [[package]]
+name = "wasite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.93"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.93"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.72",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.93"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.93"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.72",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.93"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
+
+[[package]]
+name = "web-sys"
+version = "0.3.70"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "whoami"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9"
+dependencies = [
+ "redox_syscall",
+ "wasite",
+ "web-sys",
+]
+
+[[package]]
 name = "winapi"
 version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1449,6 +2819,70 @@
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
 [[package]]
+name = "windows"
+version = "0.58.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
+dependencies = [
+ "windows-core",
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.58.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-result",
+ "windows-strings",
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.58.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.72",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.58.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.72",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
+dependencies = [
+ "windows-result",
+ "windows-targets",
+]
+
+[[package]]
 name = "windows-sys"
 version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1564,7 +2998,7 @@
  "hex",
  "nix",
  "ordered-stream",
- "rand",
+ "rand 0.8.5",
  "serde",
  "serde_repr",
  "sha1",
@@ -1638,6 +3072,12 @@
 ]
 
 [[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+
+[[package]]
 name = "zvariant"
 version = "4.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,6 +2,6 @@
 members = ["cmds/*", "crates/*"]
 resolver = "2"
 
-[workspace.packages]
+[workspace.dependencies]
 bifrostlink = { path = "../bifrostlink/crates/bifrostlink" }
 bifrostlink-ports = { path = "../bifrostlink/crates/bifrostlink-ports" }
addedJustfilediffbeforeafterboth
--- /dev/null
+++ b/Justfile
@@ -0,0 +1,12 @@
+dev: dev-build dev-install
+
+dev-install:
+	mkdir -p ./target/libexec
+	ln -sf ./target/x86_64-unknown-linux-musl/release/remowt-agent ./target/libexec/remowt-x86_64-linux
+
+dev-build:
+	cargo build --release --target=x86_64-unknown-linux-musl -p remowt-agent
+
+dev-deploy: dev-build dev-install
+	ssh edgeworth2 mkdir -p /home/lach/.remowt
+	rsync -arv ./target/x86_64-unknown-linux-musl/release/remowt-agent edgeworth2:/home/lach/.remowt/remowt-agent
deletedcmds/polkit-backend/Cargo.tomldiffbeforeafterboth
--- a/cmds/polkit-backend/Cargo.toml
+++ /dev/null
@@ -1,17 +0,0 @@
-[package]
-name = "polkit-backend"
-version = "0.1.0"
-edition = "2021"
-
-[dependencies]
-anyhow = "1.0.86"
-clap = { version = "4.5.11", features = ["derive"] }
-nix = "0.29.0"
-pam-client = "0.5.0"
-polkit-shared = { version = "0.1.0", path = "../../crates/polkit-shared" }
-tokio = { version = "1.39.2", features = ["macros", "rt", "rt-multi-thread"] }
-tracing = "0.1.40"
-tracing-subscriber = "0.3.18"
-ui-prompt = { version = "0.1.0", path = "../../crates/ui-prompt" }
-zbus = { version = "4.4.0", features = ["tokio"] }
-zbus_polkit = { version = "4.0.0", features = ["tokio"] }
deletedcmds/polkit-backend/etc/systemd/system/remowt-polkit-helper.servicediffbeforeafterboth
--- a/cmds/polkit-backend/etc/systemd/system/remowt-polkit-helper.service
+++ /dev/null
@@ -1,12 +0,0 @@
-[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
deletedcmds/polkit-backend/share/dbus-1/system-services/lach.polkit.helper1.confdiffbeforeafterboth
--- a/cmds/polkit-backend/share/dbus-1/system-services/lach.polkit.helper1.conf
+++ /dev/null
@@ -1,5 +0,0 @@
-[D-BUS Service]
-Name=lach.polkit.helper1
-Exec=/bin/false
-User=root
-SystemdService=dbus-lach.polkit.helper1.service
deletedcmds/polkit-backend/share/dbus-1/system.d/lach.polkit.helper1.confdiffbeforeafterboth
--- a/cmds/polkit-backend/share/dbus-1/system.d/lach.polkit.helper1.conf
+++ /dev/null
@@ -1,12 +0,0 @@
-<?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>
deletedcmds/polkit-backend/src/main.rsdiffbeforeafterboth
--- a/cmds/polkit-backend/src/main.rs
+++ /dev/null
@@ -1,238 +0,0 @@
-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 polkit_shared::BackendRequest;
-use tokio::runtime::Handle;
-use tokio::task::{block_in_place, spawn_blocking};
-use tracing::trace;
-use ui_prompt::dbus::DbusPrompterProxyBlocking;
-use ui_prompt::{BlockingPrompter, Prompter};
-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 uid: u32 = (&reply["UnixUserID"]).try_into().unwrap();
-
-        let blocking_connection = self.blocking_connection.clone();
-        let thread_result: fdo::Result<()> = block_in_place(move || {
-            trace!("find user");
-            let user = User::from_uid(Uid::from_raw(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(
-                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
-}
addedcmds/polkit-dbus-helper/Cargo.tomldiffbeforeafterboth
--- /dev/null
+++ b/cmds/polkit-dbus-helper/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "polkit-backend"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+anyhow = "1.0.86"
+clap = { version = "4.5.11", features = ["derive"] }
+nix = "0.29.0"
+pam-client = "0.5.0"
+polkit-shared = { version = "0.1.0", path = "../../crates/polkit-shared" }
+tokio = { version = "1.39.2", features = ["macros", "rt", "rt-multi-thread"] }
+tracing = "0.1.40"
+tracing-subscriber = "0.3.18"
+ui-prompt = { version = "0.1.0", path = "../../crates/ui-prompt" }
+zbus = { version = "4.4.0", features = ["tokio"] }
+zbus_polkit = { version = "4.0.0", features = ["tokio"] }
addedcmds/polkit-dbus-helper/README.adocdiffbeforeafterboth

no changes

addedcmds/polkit-dbus-helper/etc/systemd/system/remowt-polkit-helper.servicediffbeforeafterboth
--- /dev/null
+++ b/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
addedcmds/polkit-dbus-helper/share/dbus-1/system-services/lach.polkit.helper1.confdiffbeforeafterboth
--- /dev/null
+++ b/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
addedcmds/polkit-dbus-helper/share/dbus-1/system.d/lach.polkit.helper1.confdiffbeforeafterboth
--- /dev/null
+++ b/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>
addedcmds/polkit-dbus-helper/src/main.rsdiffbeforeafterboth
--- /dev/null
+++ b/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 polkit_shared::BackendRequest;
+use tokio::task::{block_in_place, spawn_blocking};
+use tracing::trace;
+use ui_prompt::dbus::DbusPrompterProxyBlocking;
+use ui_prompt::BlockingPrompter;
+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
+}
modifiedcmds/remowt-agent/Cargo.tomldiffbeforeafterboth
--- a/cmds/remowt-agent/Cargo.toml
+++ b/cmds/remowt-agent/Cargo.toml
@@ -5,12 +5,18 @@
 
 [dependencies]
 anyhow = "1.0.86"
+bifrostlink.workspace = true
+bifrostlink-ports.workspace = true
 clap = { version = "4.5.13", features = ["derive"] }
-pam-client = "0.5.0"
+futures = "0.3.30"
+futures-util = "0.3.30"
+nix = "0.29.0"
 polkit-shared = { version = "0.1.0", path = "../../crates/polkit-shared" }
 rand = "0.8.5"
+remowt-link-shared = { version = "0.1.0", path = "../../crates/remowt-link-shared" }
 serde = { version = "1.0.204", features = ["derive"] }
 tokio = { version = "1.39.2", features = ["rt-multi-thread", "fs", "macros"] }
+tokio-util = { version = "0.7.11", features = ["codec"] }
 tracing = "0.1.40"
 tracing-subscriber = "0.3.18"
 ui-prompt = { version = "0.1.0", path = "../../crates/ui-prompt" }
addedcmds/remowt-agent/src/helper/dbus.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/remowt-agent/src/helper/dbus.rs
@@ -0,0 +1,79 @@
+use std::collections::HashMap;
+use std::marker::PhantomData;
+
+use polkit_shared::{BackendRequest, Identity};
+use tokio::runtime::Handle;
+use ui_prompt::dbus::DbusPrompterInterface;
+use 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) {
+        // FIXME: block_in_place prevents to moving to current_thread runtime
+        // There should be a blocking way to remove ObjectServer listener.
+        // As far as I can see, it is only async because of async RwLock, shouldn't it be
+        // just a sync lock?
+        tokio::task::block_in_place(move || {
+            Handle::current().block_on(async {
+                let _ = self
+                    .connection
+                    .object_server()
+                    .remove::<DbusPrompterInterface<P>, String>(self.path.clone())
+                    .await;
+            });
+        });
+    }
+}
+
+pub struct DbusHelper {
+    connection: Connection,
+    helper: PolkitHelperProxy<'static>,
+}
+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(())
+    }
+}
addedcmds/remowt-agent/src/helper/mod.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/remowt-agent/src/helper/mod.rs
@@ -0,0 +1,18 @@
+use futures::Future;
+use polkit_shared::Identity;
+use ui_prompt::Prompter;
+
+mod suid;
+mod dbus;
+
+pub use suid::SuidHelper;
+pub use dbus::DbusHelper;
+
+pub trait Helper {
+    fn help_me<P: Prompter + Send + Sync + 'static>(
+        &self,
+        cookie: &str,
+        prompt: P,
+        identity: Identity,
+    ) -> impl Future<Output = anyhow::Result<()>> + Send;
+}
addedcmds/remowt-agent/src/helper/suid.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/remowt-agent/src/helper/suid.rs
@@ -0,0 +1,83 @@
+use std::pin::pin;
+use std::process::Stdio;
+
+use anyhow::{bail, anyhow};
+use futures::stream::Peekable;
+use futures::StreamExt as _;
+use nix::unistd::User;
+use polkit_shared::Identity;
+use tokio::io::AsyncWriteExt as _;
+use tokio::process::Command;
+use tokio::select;
+use tokio_util::codec::{FramedRead, LinesCodec};
+use ui_prompt::Prompter;
+
+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) = dbg!(identity.uid()) else {
+            bail!("can't process identity");
+        };
+        let user = User::from_uid(dbg!(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 mut stdout =
+            pin!(
+                FramedRead::new(child.stdout.take().expect("piped"), LinesCodec::new()).peekable()
+            );
+
+        assert!(!cookie.contains("\n"));
+        stdin.write_all(cookie.as_bytes()).await?;
+        stdin.write_all(b"\n").await?;
+
+        while let Some(line) = stdout.next().await {
+            let line = dbg!(line?);
+            // TODO: Dedicated codec?
+            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(stdout.as_mut()) => {},
+                    r = prompt.display_text(false, msg_text, &[]) => {r?}
+                }
+                String::new()
+            } else if line == "SUCCESS" {
+                return Ok(());
+            } else if line == "FAILURE" {
+                bail!("helper binary reported failure")
+            } else {
+                // TODO: Success/failure handling
+                bail!("unknown agent request");
+            };
+
+            if res.contains("\n") {
+                bail!("response should not include newline")
+            }
+
+            stdin.write_all(res.as_bytes()).await?;
+            stdin.write_all(b"\n").await?;
+        }
+        bail!("agent finished unexpectedly")
+    }
+}
modifiedcmds/remowt-agent/src/main.rsdiffbeforeafterboth
--- a/cmds/remowt-agent/src/main.rs
+++ b/cmds/remowt-agent/src/main.rs
@@ -1,281 +1,269 @@
 use std::borrow::Cow;
 use std::collections::{BTreeMap, HashMap};
+use std::future;
 use std::io::{stdout, Write};
-use std::marker::PhantomData;
+use std::path::PathBuf;
 use std::sync::{Arc, Mutex, OnceLock};
-use std::{future, process};
 
+use bifrostlink::{AddressT, Rpc};
+use bifrostlink_ports::unix_socket::from_socket;
 use clap::Parser;
 use polkit_shared::{emphasize, BackendRequest, Identity, PidDisplay};
-use tokio::runtime::Handle;
-use tokio::task::{AbortHandle, JoinHandle, LocalSet};
+use remowt_link_shared::Address;
+use tokio::io::{AsyncReadExt, AsyncWriteExt};
+use tokio::net::UnixStream;
+use tokio::runtime::Runtime;
+use tokio::task::AbortHandle;
 use tracing::{info, trace};
-use ui_prompt::dbus::DbusPrompterInterface;
 use ui_prompt::rofi::RofiPrompter;
 use ui_prompt::{PrependSourcePrompter, Prompter, Source};
+use zbus::fdo;
 use zbus::zvariant::{OwnedValue, Str};
-use zbus::{fdo, ObjectServer};
 use zbus::{interface, proxy, Connection};
 use zbus_polkit::policykit1::Subject;
 
-struct TemporaryPrompterInterface<P: Prompter + Send + Sync + 'static> {
-    connection: Connection,
-    path: String,
-    _marker: PhantomData<P>,
-}
-impl<P: Prompter + Send + Sync + '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) {
-        // FIXME: block_in_place prevents to moving to current_thread runtime
-        // There should be a blocking way to remove ObjectServer listener.
-        // As far as I can see, it is only async because of async RwLock, shouldn't it be
-        // just a sync lock?
-        tokio::task::block_in_place(move || {
-            Handle::current().block_on(async {
-                let _ = self
-                    .connection
-                    .object_server()
-                    .remove::<DbusPrompterInterface<P>, String>(self.path.clone())
-                    .await;
-            });
-        });
-    }
-}
+use self::helper::{Helper, SuidHelper};
 
+pub mod helper;
+
 struct CancelTaskOnDrop {
-    tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,
-    handle: String,
+	tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,
+	handle: String,
 }
 impl Drop for CancelTaskOnDrop {
-    fn drop(&mut self) {
-        info!("cancel on drop");
-        if let Some(task) = self
-            .tasks
-            .lock()
-            .expect("not poisoned")
-            .remove(&self.handle)
-        {
-            task.abort();
-        }
-    }
+	fn drop(&mut self) {
+		info!("cancel on drop");
+		if let Some(task) = self
+			.tasks
+			.lock()
+			.expect("not poisoned")
+			.remove(&self.handle)
+		{
+			task.abort();
+		}
+	}
 }
 
-struct Agent {
-    helper: PolkitHelperProxy<'static>,
-    tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,
-    connection: Connection,
+struct Agent<H> {
+	tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,
+	helper: H,
 }
-impl Agent {
-    async fn new(connection: Connection) -> anyhow::Result<Self> {
-        Ok(Self {
-            helper: PolkitHelperProxy::new(&connection).await?,
-            tasks: Arc::new(Mutex::new(HashMap::new())),
-            connection,
-        })
-    }
+impl<H> Agent<H> {
+	fn new(helper: H) -> Self {
+		Agent {
+			tasks: Arc::new(Mutex::new(HashMap::new())),
+			helper,
+		}
+	}
 }
 
 #[interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]
-impl Agent {
-    /// 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;
-        info!("begin auth");
-        let _cancel_guard = Arc::new(OnceLock::new());
-        let task = {
-            let connection = self.connection.clone();
-            let helper = self.helper.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: RofiPrompter,
-                };
+impl<H> Agent<H>
+where
+	H: Helper + 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;
+		info!("begin auth");
+		let _cancel_guard = Arc::new(OnceLock::new());
+		let task = {
+			let helper = self.helper.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: RofiPrompter,
+				};
 
-                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();
-                info!("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?
-                    }
-                };
-                info!("identity chosen");
+				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();
+				info!("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?
+					}
+				};
+				info!("identity chosen");
+
+				let _ = write!(
+					description,
+					"\n<b>Identity:</b> {}",
+					identities[choosen_identity as usize]
+				);
+				prompter.description = description;
 
-                let _ = write!(
-                    description,
-                    "\n<b>Identity:</b> {}",
-                    identities[choosen_identity as usize]
-                );
-                prompter.description = description;
+				prompter.source.push(Source(Cow::Borrowed("polkit daemon")));
 
-                prompter.source.push(Source(Cow::Borrowed("polkit daemon")));
-                // let connection = Connection::system().await?;
-                // let helper = PolkitHelperProxy::new(&connection).await?;
-                let prompter = TemporaryPrompterInterface::new(connection, prompter).await;
-                info!("init conv");
-                helper
-                    .init_conversation(
-                        BackendRequest {
-                            cookie: cookie.to_owned(),
-                            environment: HashMap::new(),
-                            prompter_path: prompter.path.clone(),
-                            // TODO: Let user choose
-                            identity: identities[choosen_identity as usize].clone(),
-                        }, // cookie.to_owned(), HashMap::new(), prompter.path.clone()
-                    )
-                    .await?;
-                println!("ASKED");
-                dbg!(action_id, message, icon_name, details, cookie, identities);
+				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());
-        info!("abort handle stored");
-        let _ = _cancel_guard.set(CancelTaskOnDrop {
-            tasks: self.tasks.clone(),
-            handle: cookie.clone(),
-        });
+				Ok(())
+			})
+		};
+		self.tasks
+			.lock()
+			.unwrap()
+			.insert(cookie.clone(), task.abort_handle());
+		info!("abort handle stored");
+		let _ = _cancel_guard.set(CancelTaskOnDrop {
+			tasks: self.tasks.clone(),
+			handle: cookie.clone(),
+		});
 
-        let _ = task.await;
+		let _ = task.await;
 
-        Ok(())
-    }
+		Ok(())
+	}
 
-    /// CancelAuthentication method
-    async fn cancel_authentication(&self, cookie: &str) -> zbus::fdo::Result<()> {
-        info!("auth cancelled");
-        if let Some(abort) = self.tasks.lock().unwrap().remove(cookie) {
-            info!("abort handle found");
-            abort.abort();
-        }
-        // debug!("Authentication cancled ! {cookie}");
-        Ok(())
-    }
+	/// CancelAuthentication method
+	async fn cancel_authentication(&self, cookie: &str) -> zbus::fdo::Result<()> {
+		info!("auth cancelled");
+		if let Some(abort) = self.tasks.lock().unwrap().remove(cookie) {
+			info!("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"
+	interface = "lach.PolkitHelper",
+	default_service = "lach.polkit.helper1",
+	default_path = "/lach/PolkitHelper"
 )]
 trait PolkitHelper {
-    fn init_conversation(&self, request: BackendRequest) -> zbus::Result<()>;
+	fn init_conversation(&self, request: BackendRequest) -> zbus::Result<()>;
 }
 
 #[derive(Parser)]
 enum Opts {
-    Agent,
-    AskPass { description: String },
+	Agent,
+	AskPass {
+		description: String,
+	},
+	RealAgent {
+		#[arg(long)]
+		path: PathBuf,
+	},
 }
 
-#[tokio::main]
-async fn main() -> anyhow::Result<()> {
-    tracing_subscriber::fmt::init();
-    let opts = Opts::parse();
+fn main() -> anyhow::Result<()> {
+	tracing_subscriber::fmt::init();
+	let opts = Opts::parse();
 
-    match opts {
-        Opts::Agent => {
-            trace!("started");
-            let conn = Connection::system().await?;
+	let runtime = Runtime::new()?;
 
-            let proxy = zbus_polkit::policykit1::AuthorityProxy::new(&conn).await?;
-            conn.object_server()
-                .at(OBJ_PATH, Agent::new(conn.clone()).await?)
-                .await?;
+	match opts {
+		Opts::Agent => {
+			// TODO: Setup env, directories with various things...
+			runtime.block_on(main_agent())
+		}
+		Opts::AskPass { description } => runtime.block_on(main_askpass(description)),
+		Opts::RealAgent { path } => runtime.block_on(main_real_agent(path)),
+	}
+}
+async fn main_real_agent(path: PathBuf) -> anyhow::Result<()> {
+    let mut stream = UnixStream::connect(path).await?;
+    stream.write_all(b"REMOWT_HELLO\0").await?;
+    let mut buf = [0u8; 12];
+    stream.read_exact(&mut buf).await?;
+    assert_eq!(&buf, b"REMOWT_EHLO\0");
+    let port = from_socket(stream);
+    let rpc = Rpc::<Address, remowt_link_shared::Error>::new(Address::Agent);
+    rpc.add_direct(Address::User, port, bifrostlink::Rtt(0));
+    Ok(())
+}
 
-            let session_id = std::env::var("XDG_SESSION_ID")?;
-            let mut details = HashMap::new();
-            let val: OwnedValue = {
-                let wrapped: Str<'_> = session_id.into();
-                wrapped.into()
-            };
-            details.insert("session-id".to_string(), val);
-            proxy
-                .register_authentication_agent(
-                    &Subject {
-                        subject_kind: "unix-session".to_string(),
-                        subject_details: details,
-                    },
-                    "C",
-                    OBJ_PATH,
-                )
-                .await?;
-        }
-        Opts::AskPass { description } => {
-            let password = RofiPrompter
-                .prompt_text(false, &description, "SSH password request", &[])
-                .await?;
-            stdout().lock().write_all(password.as_bytes())?;
-        }
-    }
+async fn main_agent() -> anyhow::Result<()> {
+	trace!("started");
+	let conn = Connection::system().await?;
+
+	let proxy = zbus_polkit::policykit1::AuthorityProxy::new(&conn).await?;
+	conn.object_server()
+		.at(OBJ_PATH, Agent::new(SuidHelper))
+		.await?;
+
+	let session_id = std::env::var("XDG_SESSION_ID")?;
+	let mut details = HashMap::new();
+	let val: OwnedValue = {
+		let wrapped: Str<'_> = session_id.into();
+		wrapped.into()
+	};
+	details.insert("session-id".to_string(), val);
+	proxy
+		.register_authentication_agent(
+			&Subject {
+				subject_kind: "unix-session".to_string(),
+				subject_details: details,
+			},
+			"C",
+			OBJ_PATH,
+		)
+		.await?;
+	future::pending().await
+}
 
-    future::pending().await
+async fn main_askpass(description: String) -> anyhow::Result<()> {
+	let password = RofiPrompter
+		.prompt_text(false, &description, "SSH password request", &[])
+		.await?;
+	stdout().lock().write_all(password.as_bytes())?;
+	future::pending().await
 }
modifiedcmds/remowt-ssh/Cargo.tomldiffbeforeafterboth
--- a/cmds/remowt-ssh/Cargo.toml
+++ b/cmds/remowt-ssh/Cargo.toml
@@ -4,3 +4,17 @@
 edition = "2021"
 
 [dependencies]
+clap = { version = "4.5.16", features = ["derive"] }
+openssh = { version = "0.11.0", features = ["native-mux"] }
+tracing-subscriber = "0.3.18"
+bifrostlink.workspace = true
+remowt-link-shared = { version = "0.1.0", path = "../../crates/remowt-link-shared" }
+tokio = { version = "1.39.3", features = ["macros"] }
+anyhow = "1.0.86"
+bifrostlink-ports.workspace = true
+uuid = { version = "1.10.0", features = ["v4"] }
+tempdir = "0.3.7"
+russh = { git = "https://github.com/Eugeny/russh/" }
+russh-config = { git = "https://github.com/Eugeny/russh/" }
+russh-keys = { git = "https://github.com/Eugeny/russh/" }
+async-trait = "0.1.81"
modifiedcmds/remowt-ssh/src/main.rsdiffbeforeafterboth
--- a/cmds/remowt-ssh/src/main.rs
+++ b/cmds/remowt-ssh/src/main.rs
@@ -1,3 +1,140 @@
-fn main() {
-    println!("Hello, world!");
+use std::borrow::Cow;
+use std::ffi::OsString;
+use std::os::unix::ffi::OsStringExt;
+use std::path::PathBuf;
+use std::sync::Arc;
+
+use anyhow::{bail, ensure};
+use async_trait::async_trait;
+use bifrostlink::Rpc;
+use clap::Parser;
+use remowt_link_shared::Address;
+use russh::client::{connect, Config, Handler, Session};
+use tempdir::TempDir;
+use tokio::io::{AsyncReadExt, AsyncWriteExt};
+use tokio::net::UnixSocket;
+
+#[derive(Parser)]
+struct Opts {
+	host: String,
+}
+
+struct MyHandler {
+	host: String,
+	port: u16,
+}
+#[async_trait]
+impl Handler for MyHandler {
+	type Error = russh::Error;
+	async fn check_server_key(
+		&mut self,
+		server_public_key: &russh_keys::key::PublicKey,
+	) -> Result<bool, Self::Error> {
+		Ok(russh_keys::check_known_hosts(
+			&self.host,
+			self.port,
+			&server_public_key,
+		)?)
+	}
+}
+
+#[tokio::main(flavor = "current_thread")]
+async fn main() -> anyhow::Result<()> {
+	let rpc = Rpc::<Address, remowt_link_shared::Error>::new(Address::User);
+	tracing_subscriber::fmt::init();
+	let opts = Opts::parse();
+
+	let conf = dbg!(russh_config::parse_home(&opts.host)?);
+	println!("connect");
+	let mut sess = connect(
+		Arc::new(Config {
+			..Default::default()
+		}),
+		dbg!((conf.host_name.clone(), conf.port)),
+		MyHandler {
+			host: conf.host_name,
+			port: conf.port,
+		},
+	)
+	.await?;
+	println!("agent");
+	let mut agent = russh_keys::agent::client::AgentClient::connect_env().await?;
+	for ele in agent.request_identities().await? {
+		let (_agent, res) = sess.authenticate_future(conf.user.clone(), ele, agent).await;
+		agent = _agent;
+		if res? {
+			break;
+		}
+	}
+	// let sess = Session::connect(opts.host, openssh::KnownHosts::Strict).await?;
+
+	let socket = UnixSocket::new_stream()?;
+
+	println!("mktemp");
+	let mut cmd_chan = sess.channel_open_session().await?;
+	cmd_chan
+		.exec(true, "mktemp -d remowt.XXXXXXXXXXXX --tmpdir")
+		.await?;
+	let mut stdout = vec![];
+	loop {
+		let Some(msg) = cmd_chan.wait().await else {
+			bail!("unexpected channel end");
+		};
+		match msg {
+			russh::ChannelMsg::Data { data } => stdout.extend(data.as_ref()),
+			russh::ChannelMsg::ExitStatus { exit_status } => {
+				if exit_status != 0 {
+					bail!("mktemp failed");
+				}
+				break;
+			}
+			_ => {}
+		}
+	}
+
+	ensure!(stdout.ends_with(b"\n"));
+	stdout.pop();
+
+	// Remote host is not neccessary linux, openssh crate makes incorrect assumptions here.
+	// TODO: Remove on local close.
+	let remote_dir = PathBuf::from(OsString::from_vec(stdout));
+	let remote_socket = remote_dir.join("primary.sock");
+
+	let local_dir = TempDir::new("remowt")?;
+	let local_socket = local_dir.path().join("primary.sock");
+
+	println!("listen");
+	socket.bind(&local_socket)?;
+	let listener = socket.listen(1)?;
+
+	eprintln!("forward socket");
+
+	let mut sock = sess
+		.channel_open_direct_streamlocal(dbg!(remote_socket.to_str().expect("path is utf-8")))
+		.await?;
+
+	eprintln!("wait");
+	while let Some(v) = sock.wait().await {
+		dbg!(v);
+	}
+
+	eprintln!("spawn agent");
+
+	// let _agent = sess
+	// 	.command("/home/lach/.remowt/remowt-agent")
+	// 	.arg("agent-real")
+	// 	.arg("--path")
+	// 	.arg(remote_socket.to_str().expect("path is utf-8"))
+	// 	.spawn()
+	// 	.await?;
+	//
+	// let (mut conn, _) = listener.accept().await?;
+	// let mut buf = [0u8; 13];
+	// conn.read_exact(&mut buf).await?;
+	// assert_eq!(&buf, b"REMOWT_HELLO\0");
+	// conn.write_all(b"REMOWT_EHLO\0").await?;
+	//
+	// println!("handshake complete!");
+
+	Ok(())
 }
modifiedcrates/polkit-shared/src/lib.rsdiffbeforeafterboth
--- a/crates/polkit-shared/src/lib.rs
+++ b/crates/polkit-shared/src/lib.rs
@@ -47,6 +47,19 @@
     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() {
addedcrates/remowt-link-shared/Cargo.tomldiffbeforeafterboth
--- /dev/null
+++ b/crates/remowt-link-shared/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "remowt-link-shared"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+bifrostlink.workspace = true
+serde = { version = "1.0.208", features = ["derive"] }
+serde_json = "1.0.125"
+thiserror = "1.0.63"
+tokio = "1.39.3"
addedcrates/remowt-link-shared/src/lib.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/remowt-link-shared/src/lib.rs
@@ -0,0 +1,39 @@
+use bifrostlink::error::{ErrorT, ListenerForYourRequestHasBeenDeadError, ResponseError};
+use bifrostlink::AddressT;
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Serialize, Hash, Eq, Debug, PartialEq, Deserialize)]
+pub enum Address {
+	User,
+    Agent,
+}
+impl AddressT for Address {}
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+    #[error("listener is dead")]
+    ListenerDead,
+    #[error("response: {0}")]
+    Response(String),
+}
+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 {}
modifiedcrates/ui-prompt/Cargo.tomldiffbeforeafterboth
--- a/crates/ui-prompt/Cargo.toml
+++ b/crates/ui-prompt/Cargo.toml
@@ -4,6 +4,7 @@
 edition = "2021"
 
 [dependencies]
+bifrostlink.workspace = true
 serde = "1.0.204"
 thiserror = "1.0.63"
 tokio = { version = "1.39.2", features = ["io-util", "macros", "process", "rt"] }
addedcrates/ui-prompt/src/bifrost.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/ui-prompt/src/bifrost.rs
@@ -0,0 +1,166 @@
+use bifrostlink::error::ErrorT;
+use bifrostlink::{request, AddressT, Rpc};
+use serde::{Deserialize, Serialize};
+
+use crate::{Error, Prompter, Source};
+
+pub struct BifrostPrompter<A: AddressT, E: ErrorT> {
+	pub address: A,
+	pub rpc: Rpc<A, E>,
+}
+
+#[derive(Serialize, Deserialize)]
+struct EnumRequest {
+	prompt: String,
+	description: String,
+	variants: Vec<String>,
+	source: Vec<Source>,
+}
+#[derive(Serialize, Deserialize)]
+struct EnumResponse {
+	value: u32,
+}
+request!(EnumRequest => EnumResponse);
+
+#[derive(Serialize, Deserialize)]
+struct TextRequest {
+	echo: bool,
+	prompt: String,
+	description: String,
+	source: Vec<Source>,
+}
+#[derive(Serialize, Deserialize)]
+struct TextResponse {
+	value: String,
+}
+request!(TextRequest => TextResponse);
+
+#[derive(Serialize, Deserialize)]
+struct DisplayRequest {
+	error: bool,
+	description: String,
+	source: Vec<Source>,
+}
+request!(DisplayRequest => ());
+
+impl<A: AddressT, E: ErrorT> Prompter for BifrostPrompter<A, E>
+where
+	crate::Error: From<E>,
+{
+	async fn prompt_enum(
+		&self,
+		prompt: &str,
+		description: &str,
+		variants: &[&str],
+		source: &[crate::Source],
+	) -> crate::Result<u32> {
+		let res = self
+			.rpc
+			.request(
+				self.address.clone(),
+				&EnumRequest {
+					prompt: prompt.to_owned(),
+					description: description.to_owned(),
+					variants: variants.into_iter().map(|v| (*v).to_owned()).collect(),
+					source: source.to_vec(),
+				},
+			)
+			.await?;
+		Ok(res.value)
+	}
+
+	async fn prompt_text(
+		&self,
+		echo: bool,
+		prompt: &str,
+		description: &str,
+		source: &[crate::Source],
+	) -> crate::Result<String> {
+		let res = self
+			.rpc
+			.request(
+				self.address.clone(),
+				&TextRequest {
+					echo,
+					prompt: prompt.to_owned(),
+					description: description.to_owned(),
+					source: source.to_vec(),
+				},
+			)
+			.await?;
+		Ok(res.value)
+	}
+
+	async fn display_text(
+		&self,
+		error: bool,
+		description: &str,
+		source: &[crate::Source],
+	) -> crate::Result<()> {
+		self.rpc
+			.request(
+				self.address.clone(),
+				&DisplayRequest {
+					error,
+					description: description.to_owned(),
+					source: source.to_vec(),
+				},
+			)
+			.await?;
+		Ok(())
+	}
+}
+
+pub fn handle_bifrost_prompts<
+	P: Prompter + Clone + 'static,
+	A: AddressT,
+	E: ErrorT + From<Error>,
+>(
+	rpc: &Rpc<A, E>,
+	prompt: P,
+) {
+	rpc.register_request_handler(true, {
+		let prompt = prompt.clone();
+		move |_addr, req: EnumRequest| {
+			let prompt = prompt.clone();
+			async move {
+				let i = prompt
+					.prompt_enum(
+						&req.prompt,
+						&req.description,
+						&req.variants.iter().map(|v| v.as_str()).collect::<Vec<_>>(),
+						&req.source,
+					)
+					.await?;
+
+				Ok(EnumResponse { value: i })
+			}
+		}
+	});
+	rpc.register_request_handler(true, {
+		let prompt = prompt.clone();
+		move |_addr, req: TextRequest| {
+			let prompt = prompt.clone();
+			async move {
+				let i = prompt
+					.prompt_text(req.echo, &req.prompt, &req.description, &req.source)
+					.await?;
+
+				Ok(TextResponse { value: i })
+			}
+		}
+	});
+	rpc.register_request_handler(true, {
+		let prompt = prompt.clone();
+		move |_addr, req: DisplayRequest| {
+			let prompt = prompt.clone();
+			async move {
+				prompt
+					.display_text(req.error, &req.description, &req.source)
+					.await?;
+
+				Ok(())
+			}
+		}
+	});
+}
modifiedcrates/ui-prompt/src/lib.rsdiffbeforeafterboth
--- a/crates/ui-prompt/src/lib.rs
+++ b/crates/ui-prompt/src/lib.rs
@@ -5,6 +5,7 @@
 
 pub mod dbus;
 pub mod rofi;
+pub mod bifrost;
 
 #[derive(thiserror::Error, Debug)]
 pub enum Error {
@@ -25,7 +26,7 @@
     }
 }
 
-pub trait Prompter {
+pub trait Prompter: Send + Sync {
     fn prompt_radio(
         &self,
         prompt: &str,
@@ -77,6 +78,48 @@
     ) -> 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,
modifiedflake.nixdiffbeforeafterboth
--- a/flake.nix
+++ b/flake.nix
@@ -62,6 +62,7 @@
             cargo-release
             rustPlatform.bindgenHook
             pam
+              just
           ];
         };
         formatter = pkgs.alejandra;
modifiedrust-toolchain.tomldiffbeforeafterboth
--- a/rust-toolchain.toml
+++ b/rust-toolchain.toml
@@ -1,3 +1,4 @@
 [toolchain]
 channel = "nightly-2024-07-20"
 components = ["rustfmt", "clippy", "rust-analyzer", "rust-src"]
+targets = ["x86_64-unknown-linux-musl"]