difftreelog
refactor join with remowt monorepo
57 files changed
Cargo.lockdiffbeforeafterboth576 packageslockfile v4
Might be heavy and slow!
adler2
2.0.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefaused byaead
0.5.2crates.io↘ 2↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0depends onaead
0.6.0crates.io↘ 2↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumef60ac202874e574ce7a7158cc8bca7313dd344322482e4fadee288bf4a306b8depends onaes
0.8.4crates.io↘ 3↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0depends onused byaes
0.9.1crates.io↘ 4↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138aes-gcm
0.10.3crates.io↘ 6↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1used byaes-gcm
0.11.0-rc.4crates.io↘ 7↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumda8c919c118108f144adecad74b425b804ad075580d605d9b33c2d6d1c62a2f8used byage
0.11.3crates.io↘ 29↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma07d86e4272c093c88caf7864a2d09af52a5159180848ca4832a3cdbd7d014d5depends on- aes
0.8.4 - aes-gcm
0.10.3 - age-core
0.11.0 - base64
0.21.7 - bcrypt-pbkdf
0.10.0 - bech32
0.9.1 - cbc
0.1.2 - chacha20poly1305
0.10.1 - cipher
0.4.4 - cookie-factory
0.3.3 - ctr
0.9.2 - curve25519-dalek
4.1.3 - hmac
0.12.1 - i18n-embed
0.15.4 - i18n-embed-fl
0.9.4 - lazy_static
1.5.0 - nom
7.1.3 - num-traits
0.2.19 - pin-project
1.1.13 - rand
0.8.6 - rsa
0.9.10 - rust-embed
8.11.0 - scrypt
0.11.0 - sha2
0.10.9 - subtle
2.6.1 - which
4.4.2 - wsl
0.1.0 - x25519-dalek
2.0.1 - zeroize
1.9.0
- aes
age-core
0.11.0crates.io↘ 10↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume2bf6a89c984ca9d850913ece2da39e1d200563b0a94b002b253beee4c5acf99depends onused byaho-corasick
1.1.4crates.io↘ 1↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301depends onandroid_system_properties
0.1.5crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311depends onused byanstream
1.0.0crates.io↘ 7↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28ddepends onanstyle
1.0.14crates.io↘ 0↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000anstyle-parse
1.0.0crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130edepends onused byanstyle-query
1.1.5crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadcdepends onused byanstyle-wincon
3.0.11crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747dused byanyhow
1.0.102crates.io↘ 0↖ 17sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842cused by- fleet
0.2.0 - fleet-base
0.1.0 - fleet-generator-helper
0.1.0 - fleet-install-secrets
0.1.0 - nix-eval
0.1.0 - prost-derive
0.14.4 - remowt-client
0.1.9 - remowt-fleet
0.1.0 - remowt-plugin
0.1.9 - remowt-plugin-fleet
0.1.0 - remowt-ui-prompt
0.1.9 - wasm-metadata
0.244.0 - wit-bindgen-core
0.51.0 - wit-bindgen-rust
0.51.0 - wit-bindgen-rust-macro
0.51.0 - wit-component
0.244.0 - wit-parser
0.244.0
- fleet
arc-swap
1.9.1crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207depends onused byargon2
0.6.0-rc.8crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7af50940b73bf4e16c15c448a2b121c63f2d68e3e54b6a8731673cb4aa0cdff5used byasync_fn_traits
0.1.1crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumbc58d489c5f2d2c5be31b9004cec7af25a70d23df4d8111715eee736234cf217depends onused byasync-broadcast
0.7.2crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532used byasync-channel
2.5.0crates.io↘ 4↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2async-executor
1.14.0crates.io↘ 6↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumc96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812adepends onused byasync-io
2.6.0crates.io↘ 10↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fcdepends onasync-lock
3.4.2crates.io↘ 3↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311async-process
2.5.0crates.io↘ 10↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumfc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75depends onused byasync-recursion
1.1.1crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11depends onused byasync-signal
0.2.14crates.io↘ 10↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485depends onused byasync-task
4.7.1crates.io↘ 0↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3deasync-trait
0.1.89crates.io↘ 3↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbbdepends onatomic-waker
1.1.2crates.io↘ 0↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0autocfg
1.5.1crates.io↘ 0↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53base16ct
1.0.0crates.io↘ 0↖ 5sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumfd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6base64
0.21.7crates.io↘ 0↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567used bybase64
0.22.1crates.io↘ 0↖ 6sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6base64ct
1.8.3crates.io↘ 0↖ 6sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06basic-toml
0.1.10crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178adepends onused bybcrypt-pbkdf
0.10.0crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2depends onused bybcrypt-pbkdf
0.11.0crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum144e573728da132683b9488acd528274c790e07fc06ff81ee29f9d8f8b1041e0depends onused bybifrostlink
0.2.7crates.io↘ 10↖ 7sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0c8ce9dc1425ee2aaffd3324548f114acccd456b9b1ffb33fe9eb9a7be6475a8depends onbifrostlink-macros
0.2.7crates.io↘ 3↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma7d071add2d3b90486fe141edb2e811f7735155d320aee3936ebd67e535d6ac1depends onbifrostlink-ports
0.2.7crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumccf6ba32d0ff83b27a242f9d2fca124ad215ddd9bf99144d66b7ca44a885de50used bybindgen
0.72.1crates.io↘ 12↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895depends onused bybitflags
2.13.0crates.io↘ 0↖ 14sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8blake2
0.11.0-rc.6crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum061f1a09225e328e1ffbb378d2d49923c0ca5fee19fb5ac1cc9c1e9d52b93690depends onused byblock-buffer
0.10.4crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71depends onused byblock-buffer
0.12.1crates.io↘ 2↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aadepends onused byblock-padding
0.3.3crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93depends onused byblock-padding
0.4.2crates.io↘ 1↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068bdepends onused byblocking
1.6.2crates.io↘ 5↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21used byblowfish
0.9.1crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7depends onused byblowfish
0.10.0crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum62ce3946557b35e71d1bbe07ec385073ce9eda05043f95de134eb578fcf1a298depends onused bybstr
1.12.1crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cabdepends onused bybumpalo
3.20.3crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649bytecount
0.6.9crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365eused bybyteorder
1.5.0crates.io↘ 0↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64bbytes
1.11.1crates.io↘ 0↖ 23sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33used by- bifrostlink
0.2.7 - bifrostlink-ports
0.2.7 - h2
0.4.14 - http
1.4.2 - http-body
1.0.1 - http-body-util
0.1.3 - hyper
1.10.1 - hyper-util
0.1.20 - opentelemetry-http
0.31.0 - pageant
0.2.1 - prost
0.14.4 - quinn
0.11.9 - quinn-proto
0.11.14 - remowt-client
0.1.9 - remowt-link-shared
0.1.9 - reqwest
0.12.28 - russh
0.61.2 - ssh-encoding
0.3.0-rc.9 - tokio
1.52.3 - tokio-util
0.7.18 - tonic
0.14.6 - tonic-prost
0.14.6 - tower-http
0.6.11
- bifrostlink
camino
1.2.2crates.io↘ 1↖ 6sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48depends oncbc
0.1.2crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6depends onused bycbc
0.2.1crates.io↘ 1↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896depends oncc
1.2.64crates.io↘ 2↖ 5sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumdad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501fdepends oncexpr
0.6.0crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766depends onused bycfg_aliases
0.2.1crates.io↘ 0↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724cfg-if
1.0.4crates.io↘ 0↖ 29sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801used by- aes
0.8.4 - async-io
2.6.0 - async-process
2.5.0 - async-signal
0.2.14 - chacha20
0.9.1 - chacha20
0.10.0 - const-hex
1.19.1 - crc32fast
1.5.0 - curve25519-dalek
4.1.3 - curve25519-dalek
5.0.0-rc.0 - getrandom
0.2.17 - getrandom
0.3.4 - getrandom
0.4.2 - hostname
0.4.2 - js-sys
0.3.102 - keccak
0.2.0 - libloading
0.8.9 - nix
0.31.3 - parking_lot_core
0.9.12 - polling
3.11.0 - polyval
0.6.2 - ring
0.17.14 - salsa20
0.11.0 - scrypt
0.12.0 - sha1
0.11.0 - sha2
0.10.9 - sha2
0.11.0 - thread_local
1.1.9 - wasm-bindgen
0.2.125
- aes
chacha20
0.9.1crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumc3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818depends onused bychacha20
0.10.0crates.io↘ 5↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601chacha20poly1305
0.10.1crates.io↘ 5↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35used bychrono
0.4.45crates.io↘ 6↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327depends oncipher
0.4.4crates.io↘ 3↖ 9sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3daddepends oncipher
0.5.2crates.io↘ 4↖ 10sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6cclang-sys
1.8.1crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4depends onused byclap
4.6.1crates.io↘ 2↖ 7sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51depends onclap_builder
4.6.0crates.io↘ 7↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069fdepends onused byclap_complete
4.6.5crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772depends onused byclap_derive
4.6.1crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9used byclap_lex
1.1.0crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumc8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9used bycmov
0.5.4crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027aused bycodespan-reporting
0.13.1crates.io↘ 3↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumaf491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681colorchoice
1.0.5crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570used byconcurrent-queue
2.5.0crates.io↘ 1↖ 5sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973depends onconsole
0.16.3crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87used byconst-hex
1.19.1crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum33e2a781ebdf4467d1428dc4593067825fb646f6871475098d8577421af73558used byconst-oid
0.9.6crates.io↘ 0↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumc2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8const-oid
0.10.2crates.io↘ 0↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55ccookie-factory
0.3.3crates.io↘ 1↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2depends onused bycore-foundation
0.10.1crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6depends onused bycore-foundation-sys
0.8.7crates.io↘ 0↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888bcpubits
0.1.1crates.io↘ 0↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06aecpufeatures
0.2.17crates.io↘ 1↖ 7sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280depends oncpufeatures
0.3.0crates.io↘ 1↖ 9sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201depends oncrc32fast
1.5.0crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511depends onused bycrossbeam-utils
0.8.21crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28used bycrypto-bigint
0.7.3crates.io↘ 9↖ 6sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum42a0d26b245348befa0c121944541476763dcc46ede886c88f9d12e1697d27c3depends oncrypto-common
0.1.7crates.io↘ 3↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60acrypto-common
0.2.2crates.io↘ 3↖ 7sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453crypto-primes
0.7.2crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum3633a51a39c69ebbaa4feaa694bd83d241e4093901c84a0963b19d9bb3f0cf8fdepends onused byctr
0.9.2crates.io↘ 1↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835depends onused byctr
0.10.1crates.io↘ 1↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumbaaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21depends onctutils
0.4.2crates.io↘ 2↖ 10sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29edepends oncursor-icon
1.2.0crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732fused bycurve25519-dalek
4.1.3crates.io↘ 8↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2bedepends oncurve25519-dalek
5.0.0-rc.0crates.io↘ 8↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum4f359e08ca85e7bd759e1fd933ff2bccd81864c60a8fba0e259c7f822b0924bfdepends oncurve25519-dalek-derive
0.1.1crates.io↘ 3↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3depends oncxx
1.0.194crates.io↘ 7↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum747d8437319e3a2f43d93b341c137927ca70c0f5dabeea7a005a73665e247c7edepends onused bycxx-build
1.0.194crates.io↘ 7↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb0f4697d190a142477b16aef7da8a99bfdc41e7e8b1687583c0d23a79c7afc1edepends onused bycxxbridge-cmd
1.0.194crates.io↘ 6↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd0956799fa8678d4c50eed028f2de1c0552ae183c76e976cf7ca8c4e36a7c328used bycxxbridge-flags
1.0.194crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum23384a836ab4f0ad98ace7e3955ad2de39de42378ab487dc28d3990392cb283aused bycxxbridge-macro
1.0.194crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume6acc6b5822b9526adfb4fc377b67128fdd60aac757cc4a741a6278603f763cfused bydata-encoding
2.11.0crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8used bydelegate
0.13.5crates.io↘ 3↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370depends onused byder
0.7.10crates.io↘ 2↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcbdepends onder
0.8.0crates.io↘ 3↖ 7sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411bderanged
0.5.8crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211cused byderivative
2.2.0crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumfcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770bdepends onused bydes
0.9.0crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum916a94e407b54f9034d71dd748234cd1e516ced6284009906ae246f177eafe5adepends onused bydigest
0.10.7crates.io↘ 4↖ 6sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292digest
0.11.3crates.io↘ 4↖ 13sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2displaydoc
0.2.6crates.io↘ 3↖ 5sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4fdepends onecdsa
0.17.0-rc.18crates.io↘ 7↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum54fb064faabbee66e1fc8e5c5a9458d4269dc2d8b638fe86a425adb2510d1a96depends oned25519
2.2.3crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53depends onused byed25519
3.0.0crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980adepends onused byed25519-dalek
2.2.0crates.io↘ 6↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9used byed25519-dalek
3.0.0-rc.0crates.io↘ 8↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb011170fe4f04665565b4110afef66774fe9ffff278f3eb5b81cc73d26e27d60depends oneither
1.16.0crates.io↘ 0↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679eelliptic-curve
0.14.0-rc.33crates.io↘ 15↖ 6sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum102d3643d30dd8b559613c5cced68317199597fffb278cdc88daa2ef7fafc935depends onencode_unicode
1.0.0crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0used byendi
1.1.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099used byenum_dispatch
0.3.13crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumaa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecdused byenumflags2
0.7.12crates.io↘ 2↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecefdepends onused byenumflags2_derive
0.7.12crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827depends onused byenv_filter
1.0.1crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eefdepends onused byenv_logger
0.11.10crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56aused byequivalent
1.0.2crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0fused byerrno
0.3.14crates.io↘ 2↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efebdepends onevent-listener
5.4.1crates.io↘ 3↖ 5sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856abevent-listener-strategy
0.5.4crates.io↘ 2↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93fastrand
2.4.1crates.io↘ 0↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6ff
0.14.0crates.io↘ 2↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489fdepends onfiat-crypto
0.2.9crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811dused byfiat-crypto
0.3.0crates.io↘ 0↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24find-crate
0.6.3crates.io↘ 1↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2depends onfind-msvc-tools
0.1.9crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582used byflate2
1.1.9crates.io↘ 2↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573cdepends onused byfleet
0.2.0workspace↘ 25↖ 0depends on- anyhow
1.0.102 - camino
1.2.2 - clap
4.6.1 - clap_complete
4.6.5 - fleet-base
0.1.0 - futures
0.3.32 - human-repr
1.1.0 - indicatif
0.18.4 - itertools
0.14.0 - nix-eval
0.1.0 - opentelemetry
0.31.0 - opentelemetry-appender-tracing
0.31.1 - opentelemetry-exporter-env
0.1.0 - opentelemetry_sdk
0.31.0 - remowt-fleet
0.1.0 - serde
1.0.228 - serde_json
1.0.150 - shlex
1.3.0 - tabled
0.20.0 - tempfile
3.27.0 - tokio
1.52.3 - tracing
0.1.44 - tracing-indicatif
0.3.14 - tracing-opentelemetry
0.32.1 - tracing-subscriber
0.3.23
- anyhow
fleet-base
0.1.0workspace↘ 26↖ 2depends on- age
0.11.3 - anyhow
1.0.102 - camino
1.2.2 - chrono
0.4.45 - clap
4.6.1 - fleet-shared
0.1.0 - futures
0.3.32 - hostname
0.4.2 - indoc
2.0.7 - itertools
0.14.0 - nix-eval
0.1.0 - nixlike
0.1.0 - nom
8.0.0 - rand
0.10.1 - remowt-client
0.1.9 - remowt-endpoints
0.1.9 - remowt-fleet
0.1.0 - remowt-link-shared
0.1.9 - remowt-ui-prompt
0.1.9 - serde
1.0.228 - serde_json
1.0.150 - tabled
0.20.0 - tempfile
3.27.0 - time
0.3.49 - tokio
1.52.3 - tracing
0.1.44
- age
fleet-generator-helper
0.1.0workspace↘ 12↖ 0fleet-install-secrets
0.1.0workspace↘ 10↖ 0fluent
0.16.1crates.io↘ 2↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumbb74634707bebd0ce645a981148e8fb8c7bccd4c33c652aeffd28bf2f96d555adepends onfluent-bundle
0.15.3crates.io↘ 8↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493depends onused byfluent-langneg
0.13.1crates.io↘ 1↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0depends onfluent-syntax
0.11.1crates.io↘ 1↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33ddepends onfnv
1.0.7crates.io↘ 0↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1used byfoldhash
0.1.5crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2used byfoldhash
0.2.0crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdbused byform_urlencoded
1.2.2crates.io↘ 1↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumcb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcfdepends onfutures
0.3.32crates.io↘ 7↖ 9sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382ddepends onfutures-channel
0.3.32crates.io↘ 2↖ 6sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1ddepends onfutures-core
0.3.32crates.io↘ 0↖ 19sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1dused by- async-broadcast
0.7.2 - async-channel
2.5.0 - async-signal
0.2.14 - futures
0.3.32 - futures-channel
0.3.32 - futures-executor
0.3.32 - futures-lite
2.6.1 - futures-util
0.3.32 - h2
0.4.14 - http-body-util
0.1.3 - hyper
1.10.1 - opentelemetry
0.31.0 - ordered-stream
0.2.0 - reqwest
0.12.28 - sync_wrapper
1.0.2 - tokio-stream
0.1.18 - tokio-util
0.7.18 - tower
0.5.3 - zbus
5.16.0
- async-broadcast
futures-executor
0.3.32crates.io↘ 3↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumbaf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579dfutures-io
0.3.32crates.io↘ 0↖ 7sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumcecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718futures-lite
2.6.1crates.io↘ 5↖ 5sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44adfutures-macro
0.3.32crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447bdepends onused byfutures-sink
0.3.32crates.io↘ 0↖ 6sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumc39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893futures-task
0.3.32crates.io↘ 0↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393futures-util
0.3.32crates.io↘ 9↖ 8sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6depends ongeneric-array
0.14.7crates.io↘ 2↖ 6sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9adepends ongeneric-array
1.4.3crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumc2e55f16dcf0e9c00efbe2e655ffe45fc98e7066b52bc92f8a79e64060a79351used bygetrandom
0.2.17crates.io↘ 5↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0used bygetrandom
0.3.4crates.io↘ 6↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fdgetrandom
0.4.2crates.io↘ 8↖ 6sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555depends onghash
0.5.1crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1depends onused byghash
0.6.0crates.io↘ 1↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5depends onglob
0.3.3crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280used byglobset
0.4.18crates.io↘ 5↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3used bygroup
0.14.0crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4depends onused byh2
0.4.14crates.io↘ 11↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733depends onused byhashbrown
0.15.5crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1depends onused byhashbrown
0.17.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84aused byheck
0.5.0crates.io↘ 0↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55eahermit-abi
0.5.2crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumfc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849cused byhex
0.4.3crates.io↘ 0↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70hex-literal
1.1.0crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1used byhkdf
0.12.4crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7depends onused byhkdf
0.13.0crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018depends onused byhmac
0.12.1crates.io↘ 1↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5edepends onhmac
0.13.0crates.io↘ 1↖ 5sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863fdepends onhome
0.5.12crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumcc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728ddepends onused byhostname
0.4.2crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbddepends onused byhttp
1.4.2crates.io↘ 2↖ 11sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425depends onhttp-body
1.0.1crates.io↘ 2↖ 6sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184depends onhttp-body-util
0.1.3crates.io↘ 5↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64aused byhttparse
1.10.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87used byhuman-repr
1.1.0crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf58b778a5761513caf593693f8951c97a5b610841e754788400f32102eefdff1used byhybrid-array
0.4.12crates.io↘ 4↖ 9sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857dahyper
1.10.1crates.io↘ 13↖ 5sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498depends onhyper-rustls
0.27.9crates.io↘ 8↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0fdepends onused byhyper-timeout
0.5.2crates.io↘ 5↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0used byhyper-util
0.1.20crates.io↘ 15↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0depends oni18n-config
0.4.8crates.io↘ 6↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63efi18n-embed
0.15.4crates.io↘ 12↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum669ffc2c93f97e6ddf06ddbe999fcd6782e3342978bb85f7d3c087c7978404c4depends onused byi18n-embed-fl
0.9.4crates.io↘ 11↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum04b2969d0b3fc6143776c535184c19722032b43e6a642d710fa3f88faec53c2ddepends onused byi18n-embed-impl
0.8.4crates.io↘ 5↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2used byiana-time-zone
0.1.65crates.io↘ 7↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470depends onused byiana-time-zone-haiku
0.1.2crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269fdepends onused byicu_collections
2.2.0crates.io↘ 6↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191cicu_locale_core
2.2.0crates.io↘ 5↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29icu_normalizer
2.2.0crates.io↘ 6↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumc56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4depends onused byicu_normalizer_data
2.2.0crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumda3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38used byicu_properties
2.2.0crates.io↘ 6↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumbee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70dedepends onicu_properties_data
2.2.0crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14used byicu_provider
2.2.0crates.io↘ 7↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421depends onid-arena
2.3.0crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954used byidna
1.1.0crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4deused byidna_adapter
1.2.2crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumcb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714depends onused byindexmap
2.14.0crates.io↘ 4↖ 11sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9indicatif
0.18.4crates.io↘ 6↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9ebdepends onindoc
2.0.7crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706depends onused byinout
0.1.4crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01depends onused byinout
0.2.2crates.io↘ 2↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7depends oninternal-russh-num-bigint
0.5.0crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumae8e22120c32fb4d19ec55fba35015f57095cd95a2e3b732e44457f5915b2ee8used byintl_pluralrules
7.0.2crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972depends onused byintl-memoizer
0.5.3crates.io↘ 2↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163fdepends onio_tee
0.1.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304used byipnet
2.12.0crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2used byis_terminal_polyfill
1.70.2crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695used byitertools
0.13.0crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186depends onused byitertools
0.14.0crates.io↘ 1↖ 5sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285depends onitoa
1.0.18crates.io↘ 0↖ 5sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682js-sys
0.3.102crates.io↘ 3↖ 12sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31keccak
0.2.0crates.io↘ 2↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aadepends onused bykem
0.3.0crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edddepends onused bylazy_static
1.5.0crates.io↘ 1↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumbbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fedepends onleb128fmt
0.1.0crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2used bylibc
0.2.186crates.io↘ 0↖ 27sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66used by- android_system_properties
0.1.5 - clang-sys
1.8.1 - console
0.16.3 - core-foundation
0.10.1 - cpufeatures
0.2.17 - cpufeatures
0.3.0 - errno
0.3.14 - getrandom
0.2.17 - getrandom
0.3.4 - getrandom
0.4.2 - hostname
0.4.2 - hyper-util
0.1.20 - libredox
0.1.17 - mio
1.2.1 - nix
0.31.3 - parking_lot_core
0.9.12 - quinn-udp
0.5.14 - rand
0.8.6 - ring
0.17.14 - rustix
0.38.44 - rustix
1.1.4 - security-framework
3.7.0 - security-framework-sys
2.17.0 - signal-hook-registry
1.4.8 - socket2
0.6.4 - tokio
1.52.3 - zbus
5.16.0
- android_system_properties
libloading
0.8.9crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55depends onused bylibm
0.2.16crates.io↘ 0↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981libredox
0.1.17crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3used bylink-cplusplus
1.0.12crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82depends onused bylinked-hash-map
0.5.6crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770fused bylinux-raw-sys
0.4.15crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807abused bylinux-raw-sys
0.12.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53used bylitemap
0.8.2crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0used bylock_api
0.4.14crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965depends onused bylog
0.4.32crates.io↘ 0↖ 16sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3alru-slab
0.1.2crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154used bymatchers
0.2.0crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9depends onused bymd5
0.8.0crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0used bymemchr
2.8.2crates.io↘ 0↖ 10sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4memoffset
0.9.1crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218adepends onused byminimal-lexical
0.2.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79aused byminiz_oxide
0.8.9crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316depends onused bymio
1.2.1crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fdaused byml-kem
0.3.2crates.io↘ 6↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66used bymodule-lattice
0.2.3crates.io↘ 3↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febeused bynix
0.31.3crates.io↘ 4↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumcf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3dnix-eval
0.1.0workspace↘ 16↖ 3nix-plugin-fleet
0.1.0workspace↘ 1↖ 0depends onnixlike
0.1.0workspace↘ 5↖ 2nom
7.1.3crates.io↘ 2↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4adepends onnom
8.0.0crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumdf9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405depends onused bynu-ansi-term
0.50.3crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5depends onused bynum-bigint
0.4.6crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9depends onused bynum-bigint-dig
0.8.6crates.io↘ 8↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7depends onused bynum-conv
0.2.2crates.io↘ 0↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441used bynum-integer
0.1.46crates.io↘ 1↖ 5sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858fdepends onnum-iter
0.1.45crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bfused bynum-traits
0.2.19crates.io↘ 2↖ 11sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841depends ononce_cell
1.21.4crates.io↘ 0↖ 10sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50once_cell_polyfill
1.70.2crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4feused byopaque-debug
0.3.1crates.io↘ 0↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumc08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381openssl-probe
0.2.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbeused byopentelemetry
0.31.0crates.io↘ 6↖ 7sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0depends onopentelemetry_sdk
0.31.0crates.io↘ 7↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bddepends onopentelemetry-appender-tracing
0.31.1crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumef6a1ac5ca3accf562b8c306fa8483c85f4390f768185ab775f242f7fe8fdcc2used byopentelemetry-exporter-env
0.1.0workspace↘ 3↖ 1used byopentelemetry-http
0.31.0crates.io↘ 5↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855dused byopentelemetry-otlp
0.31.1crates.io↘ 12↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71fdepends onopentelemetry-proto
0.31.0crates.io↘ 9↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30fdepends onused byordered-stream
0.2.0crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50depends onused byp256
0.14.0-rc.10crates.io↘ 5↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum41adc63effe99d48837a8cc0e6d7a77e32ae6a07f6000df466178dbc2193093edepends onp384
0.14.0-rc.10crates.io↘ 6↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9bd5333afa5ae0347f39e6a0f2c9c155da431583fd71fe5555bd0521b4ccaf02depends onp521
0.14.0-rc.10crates.io↘ 6↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma3a5297f53dc16d35909060ba3032cff7867e8809f01e273ff325579d5f0ceaedepends onpageant
0.2.1crates.io↘ 12↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum4f3a5ae18f65a85c67a77d18d42d3606c07948e3c17c1e5f74852b26589e88a5depends onused bypapergrid
0.17.0crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum6978128c8b51d8f4080631ceb2302ab51e32cc6e8615f735ee2f83fd269ae3f1depends onused byparking
2.2.1crates.io↘ 0↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dcebaparking_lot
0.12.5crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1adepends onused byparking_lot_core
0.9.12crates.io↘ 5↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1used bypassword-hash
0.6.1crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumaab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723bdepends onused bypaste
1.0.15crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0aused bypbkdf2
0.12.2crates.io↘ 2↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2depends onpbkdf2
0.13.0crates.io↘ 2↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629depends onpeg
0.8.6crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0aad070be5b63aa72103f2fcdd70a83adbd5e90112ce5b574171ff1c65501773depends onused bypeg-macros
0.8.6crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumddd8ef6825cae95355031ae26a99b616a2a21f22ba2de0197c43dfb05acbe7eeused bypeg-runtime
0.8.6crates.io↘ 0↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7011d97b484a5ebdc4b1fdb3b12d5e4bbbea56e9d22b688f2e79e04b65a7d8a6used bypem-rfc7468
1.0.0crates.io↘ 1↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9depends onpercent-encoding
2.3.2crates.io↘ 0↖ 6sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220phc
0.6.1crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892depends onused bypin-project
1.1.13crates.io↘ 1↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924depends onused bypin-project-internal
1.1.13crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumc96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8bdepends onused bypin-project-lite
0.2.17crates.io↘ 0↖ 23sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cdused by- async-broadcast
0.7.2 - async-channel
2.5.0 - async-executor
1.14.0 - async-lock
3.4.2 - event-listener
5.4.1 - event-listener-strategy
0.5.4 - futures-lite
2.6.1 - futures-util
0.3.32 - http-body-util
0.1.3 - hyper
1.10.1 - hyper-timeout
0.5.2 - hyper-util
0.1.20 - opentelemetry
0.31.0 - ordered-stream
0.2.0 - polling
3.11.0 - quinn
0.11.9 - reqwest
0.12.28 - tokio
1.52.3 - tokio-stream
0.1.18 - tokio-util
0.7.18 - tower
0.5.3 - tower-http
0.6.11 - tracing
0.1.44
- async-broadcast
piper
0.2.5crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumc835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1used bypkcs1
0.7.5crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumc8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47fdepends onused bypkcs1
0.8.0-rc.4crates.io↘ 2↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088eused bypkcs5
0.8.0crates.io↘ 8↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum279a91971a1d8eb1260a30938eae3be9cb67b472dffecb222fbbbe2fd2dc1453used bypkcs8
0.10.2crates.io↘ 2↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7pkcs8
0.11.0crates.io↘ 4↖ 5sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7pkg-config
0.3.33crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569eused byplain
0.2.3crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6used bypolling
3.11.0crates.io↘ 6↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218depends onused bypoly1305
0.8.0crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abfused bypoly1305
0.9.0crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma00baa632505d05512f48a963e16051c54fda9a95cc9acea1a4e3c90991c4a2eused bypolyval
0.6.2crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25used bypolyval
0.7.1crates.io↘ 3↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7dfc63250416fea14f5749b90725916a6c903f599d51cb635aa7a52bfd03eedeused byportable-atomic
1.13.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumc33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49used bypotential_utf
0.1.5crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564depends onused bypowerfmt
0.2.0crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391used byppv-lite86
0.2.21crates.io↘ 1↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9depends onprettyplease
0.2.37crates.io↘ 2↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62bdepends onprimefield
0.14.0-rc.11crates.io↘ 6↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb1d7e42f46a29abc16fb621a3466ee453358ebaae48a9e515f287e0af052ed8fprimeorder
0.14.0-rc.10crates.io↘ 1↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7d2793f22b9b6fd11ef3ac1d59bf003c2573593e4968702341605c2748fd90bfdepends onproc-macro-crate
3.5.0crates.io↘ 1↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614fdepends onproc-macro-error-attr2
2.0.0crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5depends onused byproc-macro-error2
2.0.1crates.io↘ 4↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802proc-macro2
1.0.106crates.io↘ 1↖ 48sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934depends onused by- async-recursion
1.1.1 - async-trait
0.1.89 - bifrostlink-macros
0.2.7 - bindgen
0.72.1 - clap_derive
4.6.1 - curve25519-dalek-derive
0.1.1 - cxx-build
1.0.194 - cxxbridge-cmd
1.0.194 - cxxbridge-macro
1.0.194 - delegate
0.13.5 - derivative
2.2.0 - displaydoc
0.2.6 - enum_dispatch
0.3.13 - enumflags2_derive
0.7.12 - futures-macro
0.3.32 - i18n-embed-fl
0.9.4 - i18n-embed-impl
0.8.4 - peg-macros
0.8.6 - pin-project-internal
1.1.13 - prettyplease
0.2.37 - proc-macro-error-attr2
2.0.0 - proc-macro-error2
2.0.1 - prost-derive
0.14.4 - quote
1.0.45 - rust-embed-impl
8.11.0 - serde_derive
1.0.228 - serde_repr
0.1.20 - syn
1.0.109 - syn
2.0.117 - synstructure
0.13.2 - tabled_derive
0.11.0 - test-log-core
0.2.21 - thiserror-impl
1.0.69 - thiserror-impl
2.0.18 - tokio-macros
2.7.0 - tracing-attributes
0.1.31 - wasm-bindgen-macro-support
0.2.125 - windows-implement
0.60.2 - windows-interface
0.59.3 - wit-bindgen-rust-macro
0.51.0 - yoke-derive
0.8.2 - zbus_macros
5.16.0 - zerocopy-derive
0.8.52 - zerofrom-derive
0.1.7 - zeroize_derive
1.5.0 - zerovec-derive
0.11.3 - zvariant_derive
5.12.0 - zvariant_utils
3.4.0
- async-recursion
proptest
1.11.0crates.io↘ 7↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744depends onused byprost
0.14.4crates.io↘ 2↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1depends onprost-derive
0.14.4crates.io↘ 5↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adfused byquinn
0.11.9crates.io↘ 12↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20depends onused byquinn-proto
0.11.14crates.io↘ 13↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098depends onused byquinn-udp
0.5.14crates.io↘ 6↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumaddec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbdused byquote
1.0.45crates.io↘ 1↖ 47sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924depends onused by- async-recursion
1.1.1 - async-trait
0.1.89 - bifrostlink-macros
0.2.7 - bindgen
0.72.1 - clap_derive
4.6.1 - curve25519-dalek-derive
0.1.1 - cxx-build
1.0.194 - cxxbridge-cmd
1.0.194 - cxxbridge-macro
1.0.194 - delegate
0.13.5 - derivative
2.2.0 - displaydoc
0.2.6 - enum_dispatch
0.3.13 - enumflags2_derive
0.7.12 - futures-macro
0.3.32 - i18n-embed-fl
0.9.4 - i18n-embed-impl
0.8.4 - peg-macros
0.8.6 - pin-project-internal
1.1.13 - proc-macro-error-attr2
2.0.0 - proc-macro-error2
2.0.1 - prost-derive
0.14.4 - rust-embed-impl
8.11.0 - serde_derive
1.0.228 - serde_repr
0.1.20 - syn
1.0.109 - syn
2.0.117 - synstructure
0.13.2 - tabled_derive
0.11.0 - test-log-core
0.2.21 - thiserror-impl
1.0.69 - thiserror-impl
2.0.18 - tokio-macros
2.7.0 - tracing-attributes
0.1.31 - wasm-bindgen-macro
0.2.125 - wasm-bindgen-macro-support
0.2.125 - windows-implement
0.60.2 - windows-interface
0.59.3 - wit-bindgen-rust-macro
0.51.0 - yoke-derive
0.8.2 - zbus_macros
5.16.0 - zerocopy-derive
0.8.52 - zerofrom-derive
0.1.7 - zeroize_derive
1.5.0 - zerovec-derive
0.11.3 - zvariant_derive
5.12.0 - zvariant_utils
3.4.0
- async-recursion
r-efi
5.3.0crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0fused byr-efi
6.0.0crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bfused byrand
0.8.6crates.io↘ 3↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8arand
0.9.4crates.io↘ 2↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76eadepends onrand
0.10.1crates.io↘ 3↖ 5sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207rand_chacha
0.3.1crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88depends onused byrand_chacha
0.9.0crates.io↘ 2↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cbdepends onused byrand_core
0.6.4crates.io↘ 1↖ 6sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922cdepends onrand_core
0.9.5crates.io↘ 1↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83cdepends onrand_core
0.10.1crates.io↘ 0↖ 20sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69used by- chacha20
0.10.0 - crypto-bigint
0.7.3 - crypto-common
0.2.2 - crypto-primes
0.7.2 - ed25519-dalek
3.0.0-rc.0 - elliptic-curve
0.14.0-rc.33 - ff
0.14.0 - getrandom
0.4.2 - group
0.14.0 - internal-russh-num-bigint
0.5.0 - kem
0.3.0 - ml-kem
0.3.2 - pkcs5
0.8.0 - pkcs8
0.11.0 - primefield
0.14.0-rc.11 - rand
0.10.1 - rsa
0.10.0-rc.18 - russh
0.61.2 - signature
3.0.0 - ssh-key
0.7.0-rc.10
- chacha20
rand_xorshift
0.4.0crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5adepends onused byredox_syscall
0.5.18crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362ddepends onused byredox_syscall
0.8.1crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7depends onused byregex
1.12.4crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6baused byregex-automata
0.4.14crates.io↘ 3↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8fregex-syntax
0.8.11crates.io↘ 0↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4remowt-client
0.1.9crates.io↘ 16↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumaf0ab07d961f14fa5c4933899a5253341e2044dec71db6e341f80b16beff2c20depends onremowt-endpoints
0.1.9crates.io↘ 9↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9a841eec11d4cc0593188062634f7ee34e87a2052a70c0fa40fbecb4aa8692c0depends onremowt-plugin
0.1.9crates.io↘ 7↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum5ca599e67dc675d8dd59abfe8f08679f7f3c6fd7aa84507ab426839985a142abdepends onused byremowt-plugin-fleet
0.1.0workspace↘ 3↖ 0remowt-ui-prompt
0.1.9crates.io↘ 8↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum2610933566f9b057dc47145b7bfc5876c4139eb766fc2fe9f2ce60baf1856c8cdepends onreqwest
0.12.28crates.io↘ 32↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumeddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147depends on- base64
0.22.1 - bytes
1.11.1 - futures-channel
0.3.32 - futures-core
0.3.32 - futures-util
0.3.32 - http
1.4.2 - http-body
1.0.1 - http-body-util
0.1.3 - hyper
1.10.1 - hyper-rustls
0.27.9 - hyper-util
0.1.20 - js-sys
0.3.102 - log
0.4.32 - percent-encoding
2.3.2 - pin-project-lite
0.2.17 - quinn
0.11.9 - rustls
0.23.40 - rustls-native-certs
0.8.4 - rustls-pki-types
1.14.1 - serde
1.0.228 - serde_json
1.0.150 - serde_urlencoded
0.7.1 - sync_wrapper
1.0.2 - tokio
1.52.3 - tokio-rustls
0.26.4 - tower
0.5.3 - tower-http
0.6.11 - tower-service
0.3.3 - url
2.5.8 - wasm-bindgen
0.2.125 - wasm-bindgen-futures
0.4.75 - web-sys
0.3.102
- base64
rfc6979
0.5.0crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum5236ce872cac07e0fb3969b0cbf468c7d2f37d432f1b627dcb7b8d34563fb0c3depends onused byring
0.17.14crates.io↘ 6↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7rsa
0.9.10crates.io↘ 12↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12ddepends onused byrsa
0.10.0-rc.18crates.io↘ 11↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum30b2aa4ba0d89f73d1e332df05be0eeab8840351c36ca5654341dfdb57bb3cafdepends onrussh
0.61.2crates.io↘ 64↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumbbf893f64684e58da8a68d56a5e84d1cf0440226274c515770fe267707a7d0b0depends on- aes
0.9.1 - bitflags
2.13.0 - block-padding
0.4.2 - byteorder
1.5.0 - bytes
1.11.1 - cbc
0.2.1 - cipher
0.5.2 - crypto-bigint
0.7.3 - ctr
0.10.1 - curve25519-dalek
5.0.0-rc.0 - data-encoding
2.11.0 - delegate
0.13.5 - der
0.8.0 - digest
0.11.3 - ecdsa
0.17.0-rc.18 - ed25519-dalek
3.0.0-rc.0 - elliptic-curve
0.14.0-rc.33 - enum_dispatch
0.3.13 - flate2
1.1.9 - futures
0.3.32 - generic-array
1.4.3 - getrandom
0.4.2 - ghash
0.6.0 - hex-literal
1.1.0 - hmac
0.13.0 - inout
0.2.2 - internal-russh-num-bigint
0.5.0 - keccak
0.2.0 - log
0.4.32 - md5
0.8.0 - ml-kem
0.3.2 - module-lattice
0.2.3 - num-bigint
0.4.6 - p256
0.14.0-rc.10 - p384
0.14.0-rc.10 - p521
0.14.0-rc.10 - pageant
0.2.1 - pbkdf2
0.13.0 - pkcs1
0.8.0-rc.4 - pkcs5
0.8.0 - pkcs8
0.11.0 - polyval
0.7.1 - rand
0.10.1 - rand_core
0.10.1 - ring
0.17.14 - rsa
0.10.0-rc.18 - russh-cryptovec
0.61.0 - russh-util
0.52.0 - salsa20
0.11.0 - scrypt
0.12.0 - sec1
0.8.1 - sha1
0.11.0 - sha2
0.11.0 - sha3
0.11.0 - signature
3.0.0 - spki
0.8.0 - ssh-encoding
0.3.0-rc.9 - ssh-key
0.7.0-rc.10 - subtle
2.6.1 - thiserror
2.0.18 - tokio
1.52.3 - typenum
1.20.1 - universal-hash
0.6.1 - zeroize
1.9.0
used by- aes
russh-config
0.58.0crates.io↘ 6↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum993346101d4741a59c82bafc6b645c52ce3e7b0820f13e4b5d48cdb591e3d955used byrussh-cryptovec
0.61.0crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum443f6bbcfacb34a1aab2b12b99bf08e0c63abdc5a0db261901365df9d57fff51used byrussh-util
0.52.0crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum668424a5dde0bcb45b55ba7de8476b93831b4aa2fa6947e145f3b053e22c60b6used byrust-embed
8.11.0crates.io↘ 3↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27used byrust-embed-impl
8.11.0crates.io↘ 5↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumda0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaaused byrust-embed-utils
8.11.0crates.io↘ 2↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1depends onrustc_version
0.4.1crates.io↘ 1↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumcfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92depends onrustc-hash
1.1.0crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2used byrustc-hash
2.1.2crates.io↘ 0↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dberustix
0.38.44crates.io↘ 5↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumfdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154used byrustix
1.1.4crates.io↘ 5↖ 7sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190rustls
0.23.40crates.io↘ 6↖ 5sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168bdepends onrustls-native-certs
0.8.4crates.io↘ 4↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumdab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4drustls-pki-types
1.14.1crates.io↘ 2↖ 5sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9depends onrustls-webpki
0.103.13crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756eused byrustversion
1.0.22crates.io↘ 0↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46dryu
1.0.23crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477fused bysalsa20
0.10.2crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213depends onused bysalsa20
0.11.0crates.io↘ 2↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42cdepends onused bysame-file
1.0.6crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502depends onused byschannel
0.1.29crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939depends onused byscopeguard
1.2.0crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49used byscratch
1.0.9crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2used byscrypt
0.11.0crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1fdepends onused byscrypt
0.12.0crates.io↘ 4↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd87af57419b594aa23fa95f09f0e06d80d84ba01c26148c43844cad6ff4485f0used bysec1
0.8.1crates.io↘ 6↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4dsecrecy
0.10.3crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5adepends onused bysecurity-framework
3.7.0crates.io↘ 5↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1ddepends onused bysecurity-framework-sys
2.17.0crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3depends onused byself_cell
0.10.3crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915ddepends onused byself_cell
1.2.2crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89used bysemver
1.0.28crates.io↘ 0↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cdserde
1.0.228crates.io↘ 2↖ 37sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9edepends onused by- basic-toml
0.1.10 - bifrostlink
0.2.7 - bstr
1.12.1 - chrono
0.4.45 - codespan-reporting
0.13.1 - ed25519-dalek
2.2.0 - ed25519-dalek
3.0.0-rc.0 - enumflags2
0.7.12 - fleet
0.2.0 - fleet-base
0.1.0 - fleet-install-secrets
0.1.0 - fleet-shared
0.1.0 - i18n-config
0.4.8 - indexmap
2.14.0 - nix-eval
0.1.0 - nixlike
0.1.0 - opentelemetry-proto
0.31.0 - remowt-client
0.1.9 - remowt-endpoints
0.1.9 - remowt-fleet
0.1.0 - remowt-link-shared
0.1.9 - remowt-ui-prompt
0.1.9 - reqwest
0.12.28 - serde_json
1.0.150 - serde_urlencoded
0.7.1 - serdect
0.4.3 - toml
0.5.11 - unic-langid-impl
0.9.6 - url
2.5.8 - wit-component
0.244.0 - wit-parser
0.244.0 - x25519-dalek
2.0.1 - zbus
5.16.0 - zbus_names
4.3.2 - zerovec
0.11.6 - zvariant
5.12.0 - zvariant_utils
3.4.0
- basic-toml
serde_core
1.0.228crates.io↘ 1↖ 9sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67addepends onserde_derive
1.0.228crates.io↘ 3↖ 5sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79depends onserde_json
1.0.150crates.io↘ 5↖ 13sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9serde_repr
0.1.20crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59cdepends onused byserde_urlencoded
0.7.1crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fdused byserdect
0.4.3crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3edepends onused bysha1
0.11.0crates.io↘ 3↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumaacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214depends onsha2
0.10.9crates.io↘ 3↖ 7sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283depends onsha2
0.11.0crates.io↘ 3↖ 11sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4depends onsha3
0.11.0crates.io↘ 2↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumbe176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1depends onused bysharded-slab
0.1.7crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6depends onused byshlex
1.3.0crates.io↘ 0↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64used bysignal-hook-registry
1.4.8crates.io↘ 2↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumc4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1bdepends onsignature
2.2.0crates.io↘ 2↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329dedepends onused bysignature
3.0.0crates.io↘ 2↖ 6sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5depends onsimd-adler32
0.3.9crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214used byslab
0.4.12crates.io↘ 0↖ 7sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5smallvec
1.15.2crates.io↘ 0↖ 8sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90socket2
0.6.4crates.io↘ 2↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51depends onspin
0.9.8crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67used byspki
0.7.3crates.io↘ 2↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29ddepends onspki
0.8.0crates.io↘ 2↖ 6sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6fdepends onssh-cipher
0.3.0-rc.9crates.io↘ 12↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum10db6f219196a8528f9ec904d9d45cdad692d65b0e57e72be4dedd1c5fddce36depends onused byssh-encoding
0.3.0-rc.9crates.io↘ 7↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7abf34aa716da5d5b4c496936d042ea282ab392092cd68a72ef6a8863ff8c96adepends onssh-key
0.7.0-rc.10crates.io↘ 18↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum45735ce3dea95690e4a9e414c4cfde7f79835063c3dcd35881df85a84118e74bdepends onused bystable_deref_trait
1.2.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596used bystrsim
0.11.1crates.io↘ 0↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4fsubtle
2.6.1crates.io↘ 0↖ 21sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292used by- aes-gcm
0.10.3 - aes-gcm
0.11.0-rc.4 - age
0.11.3 - crypto-bigint
0.7.3 - ctutils
0.4.2 - curve25519-dalek
4.1.3 - curve25519-dalek
5.0.0-rc.0 - digest
0.10.7 - ed25519-dalek
2.2.0 - ed25519-dalek
3.0.0-rc.0 - elliptic-curve
0.14.0-rc.33 - ff
0.14.0 - group
0.14.0 - hybrid-array
0.4.12 - primefield
0.14.0-rc.11 - rfc6979
0.5.0 - rsa
0.9.10 - russh
0.61.2 - rustls
0.23.40 - sec1
0.8.1 - universal-hash
0.5.1
- aes-gcm
syn
1.0.109crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237used bysyn
2.0.117crates.io↘ 3↖ 44sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99used by- async-recursion
1.1.1 - async-trait
0.1.89 - bifrostlink-macros
0.2.7 - bindgen
0.72.1 - clap_derive
4.6.1 - curve25519-dalek-derive
0.1.1 - cxx-build
1.0.194 - cxxbridge-cmd
1.0.194 - cxxbridge-macro
1.0.194 - delegate
0.13.5 - displaydoc
0.2.6 - enum_dispatch
0.3.13 - enumflags2_derive
0.7.12 - futures-macro
0.3.32 - i18n-embed-fl
0.9.4 - i18n-embed-impl
0.8.4 - pin-project-internal
1.1.13 - prettyplease
0.2.37 - proc-macro-error2
2.0.1 - prost-derive
0.14.4 - rust-embed-impl
8.11.0 - serde_derive
1.0.228 - serde_repr
0.1.20 - synstructure
0.13.2 - tabled_derive
0.11.0 - test-log-core
0.2.21 - test-log-macros
0.2.21 - thiserror-impl
1.0.69 - thiserror-impl
2.0.18 - tokio-macros
2.7.0 - tracing-attributes
0.1.31 - wasm-bindgen-macro-support
0.2.125 - windows-implement
0.60.2 - windows-interface
0.59.3 - wit-bindgen-rust
0.51.0 - wit-bindgen-rust-macro
0.51.0 - yoke-derive
0.8.2 - zbus_macros
5.16.0 - zerocopy-derive
0.8.52 - zerofrom-derive
0.1.7 - zeroize_derive
1.5.0 - zerovec-derive
0.11.3 - zvariant_derive
5.12.0 - zvariant_utils
3.4.0
- async-recursion
sync_wrapper
1.0.2crates.io↘ 1↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263depends onsynstructure
0.13.2crates.io↘ 3↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2depends ontabled
0.20.0crates.io↘ 3↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume39a2ee1fbcd360805a771e1b300f78cc88fec7b8d3e2f71cd37bbf23e725c7dused bytabled_derive
0.11.0crates.io↘ 5↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0ea5d1b13ca6cff1f9231ffd62f15eefd72543dab5e468735f1a456728a02846used bytempfile
3.27.0crates.io↘ 5↖ 7sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bdtermcolor
1.4.1crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755depends onused byterminal_size
0.4.4crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874depends onused bytest-log
0.2.21crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9b9c218384242b5c89b68303ab6f6fc53a312d923f0c14dc6bb860c6aeee40f1used bytest-log-core
0.2.21crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumc26ef8b00e4d382e59f6a8ddb3cd790b3a5bb29f21a358a9a69ea2f29f13f27bdepends onused bytest-log-macros
0.2.21crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum944ad38adcbb71eaa682c56bceeb079e4ca82b4b3edc2a0fde5cb297b77dac8ddepends onused bytesting_table
0.3.0crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0f8daae29995a24f65619e19d8d31dea5b389f3d853d8bf297bbf607cd0014ccdepends onused bythiserror
1.0.69crates.io↘ 1↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52depends onthiserror
2.0.18crates.io↘ 1↖ 14sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4depends onthiserror-impl
1.0.69crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1depends onused bythiserror-impl
2.0.18crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5depends onused bythread_local
1.1.9crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185depends onused bytime
0.3.49crates.io↘ 6↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469used bytime-core
0.1.9crates.io↘ 0↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109used bytime-macros
0.2.29crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17ddepends onused bytinystr
0.8.3crates.io↘ 3↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumc8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96dtinyvec
1.11.0crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3depends onused bytinyvec_macros
0.1.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20used bytokio
1.52.3crates.io↘ 8↖ 28sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffedepends onused by- bifrostlink
0.2.7 - bifrostlink-ports
0.2.7 - fleet
0.2.0 - fleet-base
0.1.0 - h2
0.4.14 - hyper
1.10.1 - hyper-rustls
0.27.9 - hyper-timeout
0.5.2 - hyper-util
0.1.20 - nix-eval
0.1.0 - opentelemetry-otlp
0.31.1 - pageant
0.2.1 - quinn
0.11.9 - remowt-client
0.1.9 - remowt-endpoints
0.1.9 - remowt-fleet
0.1.0 - remowt-link-shared
0.1.9 - remowt-plugin
0.1.9 - remowt-ui-prompt
0.1.9 - reqwest
0.12.28 - russh
0.61.2 - russh-config
0.58.0 - russh-util
0.52.0 - tokio-rustls
0.26.4 - tokio-stream
0.1.18 - tokio-util
0.7.18 - tonic
0.14.6 - tower
0.5.3
- bifrostlink
tokio-macros
2.7.0crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496depends onused bytokio-rustls
0.26.4crates.io↘ 2↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61depends ontokio-stream
0.1.18crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70used bytokio-util
0.7.18crates.io↘ 5↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098toml
0.5.11crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234depends onused bytoml_datetime
1.1.1+spec-1.1.0crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7depends onused bytoml_edit
0.25.12+spec-1.1.0crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7used bytoml_parser
1.1.2+spec-1.1.0crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526depends onused bytonic
0.14.6crates.io↘ 19↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445efdepends ontonic-prost
0.14.6crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0depends onused bytower
0.5.3crates.io↘ 11↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4depends ontower-http
0.6.11crates.io↘ 10↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840depends onused bytower-layer
0.3.3crates.io↘ 0↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780etower-service
0.3.3crates.io↘ 0↖ 7sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3tracing
0.1.44crates.io↘ 3↖ 25sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100used by- bifrostlink
0.2.7 - bifrostlink-ports
0.2.7 - fleet
0.2.0 - fleet-base
0.1.0 - fleet-install-secrets
0.1.0 - h2
0.4.14 - hyper-util
0.1.20 - nix-eval
0.1.0 - opentelemetry
0.31.0 - opentelemetry-appender-tracing
0.31.1 - opentelemetry-otlp
0.31.1 - quinn
0.11.9 - quinn-proto
0.11.14 - quinn-udp
0.5.14 - remowt-client
0.1.9 - remowt-endpoints
0.1.9 - remowt-fleet
0.1.0 - remowt-link-shared
0.1.9 - remowt-ui-prompt
0.1.9 - tonic
0.14.6 - tower
0.5.3 - tracing-indicatif
0.3.14 - tracing-opentelemetry
0.32.1 - tracing-subscriber
0.3.23 - zbus
5.16.0
- bifrostlink
tracing-attributes
0.1.31crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8dadepends onused bytracing-core
0.1.36crates.io↘ 2↖ 6sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumdb97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79adepends ontracing-indicatif
0.3.14crates.io↘ 4↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume1ef6990e0438749f0080573248e96631171a0b5ddfddde119aa5ba8c3a9c47eused bytracing-log
0.2.0crates.io↘ 3↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3tracing-opentelemetry
0.32.1crates.io↘ 8↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fcdepends onused bytracing-subscriber
0.3.23crates.io↘ 10↖ 7sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumcb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319depends ontype-map
0.5.1crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumcb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90depends onused bytypenum
1.20.1crates.io↘ 0↖ 5sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20uds_windows
1.2.1crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6eused byunarray
0.1.4crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumeaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94used byunic-langid
0.9.6crates.io↘ 1↖ 8sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05depends onunic-langid-impl
0.9.6crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumdce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658depends onused byunicase
2.9.0crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumdbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142used byunicode_categories
0.1.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14eused byunicode-ident
1.0.24crates.io↘ 0↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75unicode-width
0.2.2crates.io↘ 0↖ 7sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254unicode-xid
0.2.6crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853used byunit-prefix
0.5.2crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3used byuniversal-hash
0.5.1crates.io↘ 2↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumfc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07eadepends onused byuniversal-hash
0.6.1crates.io↘ 2↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96depends onuntrusted
0.9.0crates.io↘ 0↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1url
2.5.8crates.io↘ 4↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eedutf8_iter
1.0.4crates.io↘ 0↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6beutf8parse
0.2.2crates.io↘ 0↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821uuid
1.23.3crates.io↘ 4↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7valuable
0.1.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65used byversion_check
0.9.5crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105aused byvt100
0.16.2crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum054ff75fb8fa83e609e685106df4faeffdf3a735d3c74ebce97ec557d5d36fd9depends onused byvte
0.15.0crates.io↘ 5↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbdused bywalkdir
2.5.0crates.io↘ 2↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4bdepends onwant
0.3.1crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumbfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0edepends onused bywasi
0.11.1+wasi-snapshot-preview1crates.io↘ 0↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44bused bywasip2
1.0.4+wasi-0.2.12crates.io↘ 1↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487depends onused bywasip3
0.4.0+wasi-0.3.0-rc-2026-01-06crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5depends onused bywasite
0.1.0crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855bused bywasm-bindgen
0.2.125crates.io↘ 5↖ 12sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1adepends onwasm-bindgen-futures
0.4.75crates.io↘ 2↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280depends onwasm-bindgen-macro
0.2.125crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3dused bywasm-bindgen-macro-support
0.2.125crates.io↘ 5↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumfecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebdused bywasm-encoder
0.244.0crates.io↘ 2↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319depends onwasm-metadata
0.244.0crates.io↘ 4↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumbb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909wasmparser
0.244.0crates.io↘ 4↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028feweb-sys
0.3.102crates.io↘ 2↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061ddepends onused byweb-time
1.1.0crates.io↘ 2↖ 5sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bbdepends onwhich
4.4.2crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7used bywhoami
1.6.1crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97ddepends onused bywinapi-util
0.1.11crates.io↘ 1↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumc2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22depends onwindows
0.62.2crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580used bywindows_aarch64_gnullvm
0.52.6crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3used bywindows_aarch64_gnullvm
0.53.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53used bywindows_aarch64_msvc
0.52.6crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469used bywindows_aarch64_msvc
0.53.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006used bywindows_i686_gnu
0.52.6crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0bused bywindows_i686_gnu
0.53.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3used bywindows_i686_gnullvm
0.52.6crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66used bywindows_i686_gnullvm
0.53.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumfa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180cused bywindows_i686_msvc
0.52.6crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66used bywindows_i686_msvc
0.53.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2used bywindows_x86_64_gnu
0.52.6crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78used bywindows_x86_64_gnu
0.53.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499used bywindows_x86_64_gnullvm
0.52.6crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0dused bywindows_x86_64_gnullvm
0.53.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1used bywindows_x86_64_msvc
0.52.6crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ecused bywindows_x86_64_msvc
0.53.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650used bywindows-collections
0.3.2crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610depends onused bywindows-core
0.62.2crates.io↘ 5↖ 5sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9debdepends onwindows-future
0.3.2crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cbused bywindows-implement
0.60.2crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddfdepends onused bywindows-interface
0.59.3crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358depends onused bywindows-link
0.2.1crates.io↘ 0↖ 12sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5windows-numerics
0.3.1crates.io↘ 2↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26depends onused bywindows-result
0.4.1crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5depends onused bywindows-strings
0.5.1crates.io↘ 1↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091depends onwindows-sys
0.52.0crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33ddepends onused bywindows-sys
0.59.0crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853bdepends onused bywindows-sys
0.60.2crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumf2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcbdepends onused bywindows-sys
0.61.2crates.io↘ 1↖ 20sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fcdepends onwindows-targets
0.52.6crates.io↘ 8↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973depends onwindows-targets
0.53.5crates.io↘ 9↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3depends onused bywindows-threading
0.2.1crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37depends onused bywinnow
1.0.3crates.io↘ 1↖ 6sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1depends onwit-bindgen
0.51.0crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumd7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5depends onwit-bindgen
0.57.1crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536eused bywit-bindgen-core
0.51.0crates.io↘ 3↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dcdepends onwit-bindgen-rust
0.51.0crates.io↘ 8↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21depends onused bywit-bindgen-rust-macro
0.51.0crates.io↘ 7↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17adepends onused bywit-component
0.244.0crates.io↘ 11↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2depends onused bywit-parser
0.244.0crates.io↘ 10↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736depends onwriteable
0.6.3crates.io↘ 0↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4x25519-dalek
2.0.1crates.io↘ 4↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumc7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277yoke
0.8.3crates.io↘ 3↖ 4sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5yoke-derive
0.8.2crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumde844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858eused byzbus
5.16.0crates.io↘ 27↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumeee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285depends on- async-broadcast
0.7.2 - async-executor
1.14.0 - async-io
2.6.0 - async-lock
3.4.2 - async-process
2.5.0 - async-recursion
1.1.1 - async-task
4.7.1 - async-trait
0.1.89 - blocking
1.6.2 - enumflags2
0.7.12 - event-listener
5.4.1 - futures-core
0.3.32 - futures-lite
2.6.1 - hex
0.4.3 - libc
0.2.186 - ordered-stream
0.2.0 - rustix
1.1.4 - serde
1.0.228 - serde_repr
0.1.20 - tracing
0.1.44 - uds_windows
1.2.1 - uuid
1.23.3 - windows-sys
0.61.2 - winnow
1.0.3 - zbus_macros
5.16.0 - zbus_names
4.3.2 - zvariant
5.12.0
- async-broadcast
zbus_macros
5.16.0crates.io↘ 7↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumadf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6depends onused byzbus_names
4.3.2crates.io↘ 3↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52ddepends onused byzerocopy
0.8.52crates.io↘ 1↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653fdepends onused byzerocopy-derive
0.8.52crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930depends onused byzerofrom
0.1.8crates.io↘ 1↖ 5sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272depends onzerofrom-derive
0.1.7crates.io↘ 4↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1used byzeroize
1.9.0crates.io↘ 1↖ 32sourceregistry+https://github.com/rust-lang/crates.io-indexchecksume13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64edepends onused by- aes
0.9.1 - aes-gcm
0.11.0-rc.4 - age
0.11.3 - block-buffer
0.12.1 - chacha20
0.10.0 - chacha20poly1305
0.10.1 - cipher
0.4.4 - cipher
0.5.2 - crypto-bigint
0.7.3 - curve25519-dalek
4.1.3 - curve25519-dalek
5.0.0-rc.0 - der
0.7.10 - der
0.8.0 - ecdsa
0.17.0-rc.18 - ed25519-dalek
2.2.0 - ed25519-dalek
3.0.0-rc.0 - elliptic-curve
0.14.0-rc.33 - hybrid-array
0.4.12 - num-bigint-dig
0.8.6 - poly1305
0.9.0 - primefield
0.14.0-rc.11 - rsa
0.9.10 - rsa
0.10.0-rc.18 - russh
0.61.2 - rustls
0.23.40 - rustls-pki-types
1.14.1 - sec1
0.8.1 - secrecy
0.10.3 - ssh-cipher
0.3.0-rc.9 - ssh-encoding
0.3.0-rc.9 - ssh-key
0.7.0-rc.10 - x25519-dalek
2.0.1
- aes
zeroize_derive
1.5.0crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328depends onused byzerotrie
0.2.4crates.io↘ 3↖ 2sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bfdepends onzerovec
0.11.6crates.io↘ 4↖ 7sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239zerovec-derive
0.11.3crates.io↘ 3↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555depends onused byzmij
1.0.21crates.io↘ 0↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksumb8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaaused byzvariant
5.12.0crates.io↘ 6↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksuma192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0zvariant_derive
5.12.0crates.io↘ 5↖ 1sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737used byzvariant_utils
3.4.0crates.io↘ 5↖ 3sourceregistry+https://github.com/rust-lang/crates.io-indexchecksum1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6
Cargo.tomldiffbeforeafterboth--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,9 +1,10 @@
[workspace]
-members = ["crates/*", "cmds/*"]
-resolver = "2"
-package.version = "0.1.0"
+members = ["crates/*", "cmds/*", "remowt/crates/*", "remowt/cmds/*"]
+resolver = "3"
+package.version = "0.1.9"
package.edition = "2024"
-package.rust-version = "1.89.0"
+package.rust-version = "1.95.0"
+package.license = "MIT"
[workspace.dependencies]
fleet-base = { path = "./crates/fleet-base" }
@@ -13,42 +14,56 @@
opentelemetry-exporter-env = { path = "./crates/opentelemetry-exporter-env" }
remowt-fleet = { path = "./crates/remowt-fleet" }
-remowt-client = "0.1.3"
-remowt-endpoints = "0.1.3"
-remowt-link-shared = "0.1.3"
-remowt-plugin = "0.1.3"
-remowt-ui-prompt = "0.1.3"
+remowt-client = { version = "0.1.9", path = "remowt/crates/remowt-client" }
+remowt-polkit-shared = { version = "0.1.9", path = "remowt/crates/polkit-shared" }
+remowt-link-shared = { version = "0.1.9", path = "remowt/crates/remowt-link-shared" }
+remowt-plugin = { version = "0.1.9", path = "remowt/crates/remowt-plugin" }
+remowt-ui-prompt = { version = "0.1.9", path = "remowt/crates/remowt-ui-prompt" }
+remowt-endpoints = { version = "0.1.9", path = "remowt/crates/remowt-endpoints" }
-bifrostlink = "0.2.3"
+bifrostlink = "0.2.0"
+bifrostlink-macros = "0.2.0"
+bifrostlink-ports = "0.2.0"
-uuid = { version = "1", features = ["v4"] }
+iroh = { version = "1.0.0", features = ["unstable-custom-transports"] }
+iroh-base = "1.0.0"
+noq-udp = { version = "1.0.0", default-features = false }
+n0-watcher = "1.0.0"
+uuid = { version = "1", features = ["v4"] }
+russh = { version = "0.61.2", default-features = false, features = [
+ "ring",
+ "flate2",
+ "rsa",
+] }
+russh-config = "0.58.0"
age = { version = "0.11", features = ["plugin", "ssh"] }
anyhow = "1.0"
base64 = "0.22.1"
bindgen = "0.72.0"
+bytes = "1.11.0"
camino = "1.2.2"
chrono = { version = "0.4.41", features = ["serde"] }
clap = { version = "4.5", features = ["derive", "env", "unicode", "wrap_help"] }
clap_complete = "4.5"
cxx = "1.0.168"
cxx-build = "1.0.168"
-ed25519-dalek = "2.1"
+ed25519-dalek = "3.0.0-rc.0"
futures = "0.3.31"
hex = "0.4.3"
-hmac = "0.12"
+hmac = "0.13.0"
hostname = "0.4.1"
human-repr = "1.1"
indicatif = "0.18"
indoc = "2.0.6"
-itertools = "0.14.0"
+itertools = "0.15.0"
linked-hash-map = "0.5.6"
nix = { version = "0.31.2", features = ["fs", "user"] }
nom = "8.0.0"
-opentelemetry = "0.31.0"
-opentelemetry-appender-tracing = "0.31.1"
-opentelemetry-otlp = { version = "0.31.0", features = ["grpc-tonic", "gzip-tonic", "http-json", "reqwest-rustls"] }
-opentelemetry_sdk = "0.31.0"
+opentelemetry = "0.32.0"
+opentelemetry-appender-tracing = "0.32.0"
+opentelemetry-otlp = { version = "0.32.0", features = ["grpc-tonic", "gzip-tonic", "http-json", "reqwest-rustls"] }
+opentelemetry_sdk = "0.32.0"
pbkdf2 = "0.12"
peg = "0.8.5"
pkg-config = "0.3.30"
@@ -56,8 +71,8 @@
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha2 = "0.10"
-shlex = "1.3"
-tabled = "0.20.0"
+shlex = "2.0.1"
+tabled = "0.21.0"
tempfile = "3.20"
test-log = { version = "0.2.19", features = ["trace"] }
thiserror = "2.0.12"
@@ -65,12 +80,16 @@
tokio = { version = "1.45.1", features = ["fs", "macros", "rt", "rt-multi-thread", "sync", "time"] }
tracing = "0.1"
tracing-indicatif = "0.3.13"
-tracing-opentelemetry = "0.32.1"
+tracing-opentelemetry = "0.33.0"
# For fixed coloring needs to be updated to https://github.com/tokio-rs/tracing/pull/3484
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "fmt"] }
unicode_categories = "0.1.1"
vte = { version = "0.15.0", features = ["ansi"] }
x25519-dalek = { version = "2.0.1", features = ["getrandom"] }
+zbus = "5.16.0"
+zbus_polkit = "5.0.0"
+tokio-util = "0.7.11"
+pam-client = "0.5.0"
[profile.dev]
panic = "abort"
cmds/remowt-plugin-fleet/src/main.rsdiffbeforeafterboth--- a/cmds/remowt-plugin-fleet/src/main.rs
+++ b/cmds/remowt-plugin-fleet/src/main.rs
@@ -1,4 +1,4 @@
-use remowt_fleet::{Nix, init_libraries};
+use remowt_fleet::{init_libraries, Nix};
fn main() -> anyhow::Result<()> {
remowt_plugin::run(|rpc| {
crates/fleet-base/src/host.rsdiffbeforeafterboth--- a/crates/fleet-base/src/host.rs
+++ b/crates/fleet-base/src/host.rs
@@ -425,9 +425,9 @@
warn!("failed to sign store paths: {e}");
}
let store = self.nix_store().await?;
+ let eval_store = eval_store();
{
let path = path.clone();
- let eval_store = eval_store();
spawn_blocking(move || eval_store.copy_to(&store, path.as_ref()))
.await
.expect("copy_to panicked")
crates/nix-eval/src/lib.rsdiffbeforeafterboth--- a/crates/nix-eval/src/lib.rs
+++ b/crates/nix-eval/src/lib.rs
@@ -565,7 +565,7 @@
#[instrument(skip(self, dst))]
pub fn copy_to(&self, dst: &Store, path: &Utf8Path) -> Result<()> {
let sp = self
- .parse_path(&path)
+ .parse_path(path)
.context("failed to parse store path")?;
let rc = with_default_context(|c, _| unsafe {
store_copy_closure(c, self.as_ptr(), dst.0, sp.as_ptr())
remowt/README.adocdiffbeforeafterboth--- /dev/null
+++ b/remowt/README.adoc
@@ -0,0 +1,3 @@
+= Remowt
+
+A framework for executing code on remote machines.
remowt/cmds/polkit-dbus-helper/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/polkit-dbus-helper/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "polkit-backend"
+description = "polkit/PAM D-Bus authentication backend for remowt"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+clap = { workspace = true, features = ["derive"] }
+nix.workspace = true
+pam-client.workspace = true
+remowt-polkit-shared.workspace = true
+tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] }
+tracing.workspace = true
+tracing-subscriber.workspace = true
+remowt-ui-prompt.workspace = true
+zbus = { workspace = true, features = ["tokio"] }
+zbus_polkit = { workspace = true, features = ["tokio"] }
remowt/cmds/polkit-dbus-helper/README.adocdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/polkit-dbus-helper/README.adoc
@@ -0,0 +1,8 @@
+== What is it?
+
+Usually, there is a `polkit-agent-helper-1` suid binary installed on the system with polkit package.
+
+This, however, an alternative to that approach, a system daemon listening for dbus requests, which works
+without using suid binaries.
+
+In future it will provide some additional features.
remowt/cmds/polkit-dbus-helper/etc/systemd/system/remowt-polkit-helper.servicediffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/polkit-dbus-helper/etc/systemd/system/remowt-polkit-helper.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=Remowt polkit helper service
+
+[Service]
+Type=dbus
+BusName=lach.polkit.helper1
+ExecStart=@libexecdir@/polkit-backend
+# TODO: Hardening
+
+[Install]
+WantedBy=multi-user.target
+Alias=dbus-lach.polkit.helper1.service
remowt/cmds/polkit-dbus-helper/share/dbus-1/system-services/lach.polkit.helper1.confdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/polkit-dbus-helper/share/dbus-1/system-services/lach.polkit.helper1.conf
@@ -0,0 +1,5 @@
+[D-BUS Service]
+Name=lach.polkit.helper1
+Exec=/bin/false
+User=root
+SystemdService=dbus-lach.polkit.helper1.service
remowt/cmds/polkit-dbus-helper/share/dbus-1/system.d/lach.polkit.helper1.confdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/polkit-dbus-helper/share/dbus-1/system.d/lach.polkit.helper1.conf
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" "https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+<busconfig>
+ <policy user="root">
+ <allow own = "lach.polkit.helper1"/>
+ <allow send_interface="lach.PolkitInputHandler"/>
+ </policy>
+ <policy context="default">
+ <allow send_destination="lach.polkit.helper1"/>
+ <deny send_interface="lach.PolkitInputHandler"/>
+ </policy>
+</busconfig>
remowt/cmds/polkit-dbus-helper/src/main.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/polkit-dbus-helper/src/main.rs
@@ -0,0 +1,236 @@
+use std::collections::{HashMap, HashSet};
+use std::ffi::{CStr, CString};
+use std::future::pending;
+use std::sync::LazyLock;
+
+use anyhow::Context as _;
+use clap::Parser;
+use nix::unistd::{setuid, Uid, User};
+use pam_client::{Context, ConversationHandler, ErrorCode, Flag};
+use remowt_polkit_shared::BackendRequest;
+use remowt_ui_prompt::dbus::DbusPrompterProxyBlocking;
+use remowt_ui_prompt::BlockingPrompter;
+use tokio::task::{block_in_place, spawn_blocking};
+use tracing::trace;
+use zbus::fdo;
+use zbus::message::Header;
+use zbus::zvariant::OwnedValue;
+use zbus::{blocking, interface, proxy, Connection};
+
+struct Helper {
+ connection: Connection,
+ blocking_connection: blocking::Connection,
+}
+
+static ALLOWED_ENVIRONMENT: LazyLock<HashSet<&str>> = LazyLock::new(|| {
+ [
+ // pam ssh agent auth
+ "SSH_AUTH_SOCK",
+ // ssh itself provides this when running PAM
+ "SSH_AUTH_INFO_0",
+ // contains user which ran sudo
+ "SUDO_USER",
+ ]
+ .into_iter()
+ .collect()
+});
+
+struct Conversation<P>(P);
+impl<P: BlockingPrompter> Conversation<P> {
+ fn prompt_inner(&self, echo: bool, prompt: &CStr) -> Result<CString, ErrorCode> {
+ trace!("do prompt");
+ let out = self
+ .0
+ .prompt_text(echo, &prompt.to_string_lossy(), "PAM prompt request", &[])
+ .map_err(|e| {
+ trace!("prompt error: {e}");
+ ErrorCode::CONV_ERR
+ })?;
+ CString::new(out).map_err(|_| ErrorCode::CONV_AGAIN)
+ }
+ fn text_inner(&self, error: bool, msg: &CStr) {
+ trace!("do text");
+ let msg = msg.to_string_lossy();
+ let _ = self.0.display_text(error, &msg, &[]);
+ }
+}
+impl<P: BlockingPrompter> ConversationHandler for Conversation<P> {
+ fn prompt_echo_on(&mut self, prompt: &CStr) -> Result<CString, ErrorCode> {
+ self.prompt_inner(true, prompt)
+ }
+
+ fn prompt_echo_off(&mut self, prompt: &CStr) -> Result<CString, ErrorCode> {
+ self.prompt_inner(false, prompt)
+ }
+
+ fn text_info(&mut self, msg: &CStr) {
+ self.text_inner(false, msg)
+ }
+
+ fn error_msg(&mut self, msg: &CStr) {
+ self.text_inner(true, msg)
+ }
+
+ fn radio_prompt(&mut self, prompt: &CStr) -> Result<bool, ErrorCode> {
+ let prompt = prompt.to_string_lossy();
+ let result = self
+ .0
+ .prompt_radio(&prompt, "PAM prompt request", &[])
+ .map_err(|_| ErrorCode::CONV_ERR)?;
+ Ok(result)
+ }
+}
+
+#[proxy(
+ default_service = "org.freedesktop.DBus",
+ default_path = "/org/freedesktop/DBus"
+)]
+trait DBus {
+ fn get_connection_credentials(&self, body: &str) -> zbus::Result<HashMap<String, OwnedValue>>;
+}
+
+#[interface(name = "lach.PolkitHelper")]
+impl Helper {
+ async fn init_conversation(
+ &self,
+ request: BackendRequest,
+ #[zbus(header)] hdr: Header<'_>,
+ ) -> fdo::Result<()> {
+ let Some(sender) = hdr.sender().map(|v| v.to_owned()) else {
+ trace!("missing sender");
+ return Err(fdo::Error::AuthFailed("missing sender".to_owned()));
+ };
+
+ let dbus = DBusProxy::new(&self.connection).await?;
+
+ // TOCTOU: sender might be already disconnected, and there might be another
+ // user with different user id here, but does it matters?
+ let reply = dbus.get_connection_credentials(&sender).await?;
+ let connection_uid: u32 = (&reply["UnixUserID"]).try_into().unwrap();
+
+ let identity = request.identity.clone();
+ let blocking_connection = self.blocking_connection.clone();
+ let thread_result: fdo::Result<()> = block_in_place(move || {
+ trace!("find user");
+ let Some(identity_uid) = identity.uid() else {
+ return Err(fdo::Error::AuthFailed("can't process identity".to_owned()));
+ };
+ let user = User::from_uid(identity_uid)
+ .map_err(|_| fdo::Error::AuthFailed("error querying user".to_owned()))?
+ .ok_or_else(|| fdo::Error::AuthFailed("uid not found".to_owned()))?;
+
+ let responder = DbusPrompterProxyBlocking::new(
+ &blocking_connection,
+ sender,
+ request.prompter_path,
+ )?;
+ let conversation = Conversation(responder);
+ trace!("run context for {}", &user.name);
+ let mut ctx = Context::new(
+ // TODO: Should another scope be used?
+ "login",
+ Some(&user.name),
+ conversation,
+ )
+ .map_err(|_| fdo::Error::Failed("pam context init failed".to_owned()))?;
+
+ trace!("fill env");
+ for (k, v) in request.environment {
+ if k.contains('=') || !ALLOWED_ENVIRONMENT.contains(k.as_str()) {
+ continue;
+ }
+ let _ = ctx.putenv(format!("{k}={v}"));
+ }
+
+ trace!("authenticate");
+ ctx.authenticate(Flag::NONE)
+ .map_err(|_| fdo::Error::AuthFailed("pam authentication failed".to_owned()))?;
+
+ trace!("acct mgmt");
+ ctx.acct_mgmt(Flag::NONE)
+ .map_err(|_| fdo::Error::AuthFailed("pam acct mgmt failed".to_owned()))?;
+
+ Ok(())
+ });
+
+ thread_result?;
+
+ trace!("respond");
+ let proxy = zbus_polkit::policykit1::AuthorityProxy::new(&self.connection).await?;
+
+ let identity_details = request
+ .identity
+ .details
+ .iter()
+ .map(|(k, v)| (k.as_str(), (**v).try_clone().expect("success")))
+ .collect::<HashMap<_, _>>();
+ proxy
+ .authentication_agent_response2(
+ connection_uid,
+ &request.cookie,
+ &zbus_polkit::policykit1::Identity {
+ identity_kind: &request.identity.kind,
+ identity_details: &identity_details,
+ },
+ )
+ .await?;
+ Ok(())
+ }
+}
+
+const OBJ_PATH: &str = "/lach/PolkitHelper";
+
+#[derive(Parser)]
+struct Opts {
+ /// Not recommended: start as a session connection, then use escalation
+ /// to respond to polkit requests.
+ #[arg(long)]
+ session: bool,
+}
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ tracing_subscriber::fmt::init();
+ let opts = Opts::parse();
+ let connection = if opts.session {
+ Connection::session().await
+ } else {
+ Connection::system().await
+ }
+ .context("failed to open connection")?;
+
+ let session = opts.session;
+ let blocking_connection: anyhow::Result<blocking::Connection> = spawn_blocking(move || {
+ Ok(if session {
+ blocking::Connection::session()?
+ } else {
+ blocking::Connection::system()?
+ })
+ })
+ .await?;
+ let blocking_connection = blocking_connection.context("failed to open blocking connection")?;
+
+ if opts.session {
+ setuid(Uid::from_raw(0))
+ .context("polkit-backend needs to be suid if run in session mode")?;
+ }
+
+ connection
+ .object_server()
+ .at(
+ OBJ_PATH,
+ Helper {
+ connection: connection.clone(),
+ blocking_connection,
+ },
+ )
+ .await
+ .context("failed listen path")?;
+
+ connection
+ .request_name("lach.polkit.helper1")
+ .await
+ .context("failed to request name")?;
+
+ pending().await
+}
remowt/cmds/remowt-agent/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-agent/Cargo.toml
@@ -0,0 +1,35 @@
+[package]
+name = "remowt-agent"
+description = "remowt on-host agent serving fs/pty/systemd endpoints over bifrostlink"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+bifrostlink.workspace = true
+bifrostlink-ports.workspace = true
+clap = { workspace = true, features = ["derive"] }
+futures.workspace = true
+nix.workspace = true
+remowt-polkit-shared.workspace = true
+remowt-link-shared.workspace = true
+remowt-plugin.workspace = true
+tempfile.workspace = true
+tokio = { workspace = true, features = [
+ "rt",
+ "fs",
+ "macros",
+ "net",
+ "io-util",
+ "time",
+ "process",
+] }
+tokio-util = { workspace = true, features = ["codec"] }
+tracing.workspace = true
+tracing-subscriber.workspace = true
+remowt-ui-prompt.workspace = true
+uuid = { workspace = true, features = ["v4"] }
+zbus = { workspace = true, features = ["tokio"] }
+zbus_polkit = { workspace = true, features = ["tokio"] }
+remowt-endpoints.workspace = true
remowt/cmds/remowt-agent/src/askpass.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-agent/src/askpass.rs
@@ -0,0 +1,51 @@
+use std::borrow::Cow;
+use std::io::Write as _;
+
+use anyhow::Context as _;
+use remowt_ui_prompt::dbus::{DbusPrompterInterface, DbusPrompterProxy, BUS_NAME, PROMPTER_PATH};
+use remowt_ui_prompt::{Prompter, Source};
+use tracing::debug;
+use zbus::Connection;
+
+pub async fn serve<P>(conn: &Connection, prompter: P) -> anyhow::Result<()>
+where
+ P: Prompter + 'static,
+{
+ conn.object_server()
+ .at(PROMPTER_PATH, DbusPrompterInterface(prompter))
+ .await?;
+ match conn.request_name(BUS_NAME).await {
+ Ok(()) => {}
+ Err(zbus::Error::NameTaken) => {
+ debug!("{BUS_NAME} already owned, chaining to upstream");
+ }
+ Err(e) => return Err(e.into()),
+ }
+ Ok(())
+}
+
+pub async fn ask(prompt: &str, description: String) -> anyhow::Result<()> {
+ let conn = Connection::session()
+ .await
+ .context("connecting to the session bus (DBUS_SESSION_BUS_ADDRESS)")?;
+ let proxy = DbusPrompterProxy::builder(&conn)
+ .destination(BUS_NAME)?
+ .path(PROMPTER_PATH)?
+ .build()
+ .await?;
+
+ let password = proxy
+ .prompt_text(
+ false,
+ prompt,
+ &description,
+ &[Source(Cow::Borrowed("remowt-askpass"))],
+ )
+ .await?;
+
+ let mut out = std::io::stdout().lock();
+ out.write_all(password.as_bytes())?;
+ out.write_all(b"\n")?;
+ out.flush()?;
+ Ok(())
+}
remowt/cmds/remowt-agent/src/bus.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-agent/src/bus.rs
@@ -0,0 +1,40 @@
+use std::process::Stdio;
+
+use anyhow::Context as _;
+use futures::StreamExt as _;
+use tokio::process::{Child, Command};
+use tokio_util::codec::{FramedRead, LinesCodec};
+use zbus::Connection;
+
+pub struct PrivateBus {
+ pub address: String,
+ pub conn: Connection,
+ _child: Child,
+}
+
+pub async fn spawn() -> anyhow::Result<PrivateBus> {
+ let mut child = Command::new("dbus-daemon")
+ .args(["--session", "--nofork", "--print-address"])
+ .stdout(Stdio::piped())
+ .kill_on_drop(true)
+ .spawn()
+ .context("spawning dbus-daemon for the private bus")?;
+
+ let stdout = child.stdout.take().expect("piped");
+ let address = FramedRead::new(stdout, LinesCodec::new())
+ .next()
+ .await
+ .context("dbus-daemon exited before printing its address")?
+ .context("reading dbus-daemon address")?;
+
+ let conn = zbus::connection::Builder::address(address.as_str())?
+ .build()
+ .await
+ .context("connecting to the private bus")?;
+
+ Ok(PrivateBus {
+ address,
+ conn,
+ _child: child,
+ })
+}
remowt/cmds/remowt-agent/src/editor.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-agent/src/editor.rs
@@ -0,0 +1,158 @@
+use std::env::{current_dir, temp_dir};
+use std::path::Path;
+use std::time::Duration;
+use std::{fs, io};
+
+use anyhow::{bail, Context as _};
+use nix::libc;
+use remowt_link_shared::editor::EditorEndpointsClient;
+use tokio::process::Command;
+use zbus::{fdo, interface, proxy, Connection};
+
+use remowt_link_shared::BifConfig;
+
+const BUS_NAME: &str = "lach.RemowtEditor";
+const SERVICE_PATH: &str = "/lach/Editor";
+
+pub struct EditorService {
+ editor: EditorEndpointsClient<BifConfig>,
+}
+
+#[interface(name = "lach.RemowtEditor")]
+impl EditorService {
+ /// Attach the User's GUI to the nvim server at `socket_path` (on the remote),
+ /// blocking until the user is done.
+ async fn edit(&self, socket_path: String) -> fdo::Result<()> {
+ self.editor
+ .open_editor(socket_path)
+ .await
+ .map_err(|e| fdo::Error::Failed(format!("requesting editor on the User: {e}")))?
+ .map_err(|e| fdo::Error::Failed(format!("editor failed: {e}")))?;
+ Ok(())
+ }
+
+ async fn forward_tcp(&self, addr: String) -> fdo::Result<u16> {
+ let local = self
+ .editor
+ .expose_tcp(addr)
+ .await
+ .map_err(|e| fdo::Error::Failed(format!("requesting tcp forward on the User: {e}")))?
+ .map_err(|e| fdo::Error::Failed(format!("tcp forward failed: {e}")))?;
+ Ok(local)
+ }
+
+ async fn forward_udp(&self, addr: String) -> fdo::Result<u16> {
+ let local = self
+ .editor
+ .expose_udp(addr)
+ .await
+ .map_err(|e| fdo::Error::Failed(format!("requesting udp forward on the User: {e}")))?
+ .map_err(|e| fdo::Error::Failed(format!("udp forward failed: {e}")))?;
+ Ok(local)
+ }
+}
+
+pub async fn serve(
+ conn: &Connection,
+ editor: EditorEndpointsClient<BifConfig>,
+) -> anyhow::Result<()> {
+ conn.object_server()
+ .at(SERVICE_PATH, EditorService { editor })
+ .await?;
+ conn.request_name(BUS_NAME).await?;
+ Ok(())
+}
+
+#[proxy(interface = "lach.RemowtEditor")]
+trait RemowtEditor {
+ async fn edit(&self, socket_path: &str) -> fdo::Result<()>;
+ async fn forward_tcp(&self, addr: &str) -> fdo::Result<u16>;
+ async fn forward_udp(&self, addr: &str) -> fdo::Result<u16>;
+}
+
+pub async fn forward(udp: bool, addr: String) -> anyhow::Result<()> {
+ let conn = Connection::session()
+ .await
+ .context("connecting to the session bus (DBUS_SESSION_BUS_ADDRESS)")?;
+ let proxy = RemowtEditorProxy::builder(&conn)
+ .destination(BUS_NAME)?
+ .path(SERVICE_PATH)?
+ .build()
+ .await?;
+ let local = if udp {
+ proxy.forward_udp(&addr).await?
+ } else {
+ proxy.forward_tcp(&addr).await?
+ };
+ println!("{local}");
+ Ok(())
+}
+
+pub async fn edit(path: String) -> anyhow::Result<()> {
+ let path = Path::new(&path);
+ let abs = if path.is_absolute() {
+ path.to_path_buf()
+ } else {
+ current_dir()?.join(path)
+ };
+
+ let sock = temp_dir().join(format!("remowt-nvim-{}.sock", uuid::Uuid::new_v4()));
+ let sock_str = sock
+ .to_str()
+ .context("temp socket path is not utf-8")?
+ .to_owned();
+
+ let mut child = Command::new("nvim");
+ child
+ .arg("--headless")
+ .arg("--listen")
+ .arg(&sock)
+ .arg("--")
+ .arg(&abs)
+ .kill_on_drop(true);
+ // SAFETY: only an async-signal-safe `prctl` call.
+ unsafe {
+ child.pre_exec(|| {
+ if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL as libc::c_ulong) != 0 {
+ return Err(io::Error::last_os_error());
+ }
+ Ok(())
+ });
+ }
+ let mut child = child.spawn().context("spawning nvim")?;
+
+ wait_for_socket(&sock)
+ .await
+ .context("nvim did not start its server")?;
+
+ let conn = Connection::session()
+ .await
+ .context("connecting to the session bus (DBUS_SESSION_BUS_ADDRESS)")?;
+ let proxy = RemowtEditorProxy::builder(&conn)
+ .destination(BUS_NAME)?
+ .path(SERVICE_PATH)?
+ .build()
+ .await?;
+ let result = proxy.edit(&sock_str).await;
+
+ if tokio::time::timeout(Duration::from_secs(2), child.wait())
+ .await
+ .is_err()
+ {
+ let _ = child.kill().await;
+ }
+ let _ = fs::remove_file(&sock);
+
+ result?;
+ Ok(())
+}
+
+async fn wait_for_socket(path: &Path) -> anyhow::Result<()> {
+ for _ in 0..200 {
+ if tokio::fs::try_exists(path).await.unwrap_or(false) {
+ return Ok(());
+ }
+ tokio::time::sleep(Duration::from_millis(50)).await;
+ }
+ bail!("timed out waiting for {}", path.display())
+}
remowt/cmds/remowt-agent/src/helper/dbus.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-agent/src/helper/dbus.rs
@@ -0,0 +1,81 @@
+use std::collections::HashMap;
+use std::marker::PhantomData;
+
+use remowt_polkit_shared::{BackendRequest, Identity};
+use remowt_ui_prompt::dbus::DbusPrompterInterface;
+use remowt_ui_prompt::Prompter;
+use zbus::Connection;
+
+use crate::PolkitHelperProxy;
+
+use super::Helper;
+
+struct TemporaryPrompterInterface<P: Prompter + 'static> {
+ connection: Connection,
+ path: String,
+ _marker: PhantomData<P>,
+}
+impl<P: Prompter + 'static> TemporaryPrompterInterface<P> {
+ async fn new(connection: Connection, prompter: P) -> Self {
+ let path = format!(
+ "/remowt/prompters/{}",
+ uuid::Uuid::new_v4().to_string().replace("-", "_")
+ );
+ let _ = connection
+ .object_server()
+ .at(path.clone(), DbusPrompterInterface(prompter))
+ .await;
+ Self {
+ connection,
+ path,
+ _marker: PhantomData,
+ }
+ }
+}
+impl<P: Prompter + Send + Sync + 'static> Drop for TemporaryPrompterInterface<P> {
+ fn drop(&mut self) {
+ // Removal is async because of async RwLock used inside...
+ // We should not care about its reuse
+ let connection = self.connection.clone();
+ let path = std::mem::take(&mut self.path);
+ tokio::spawn(async move {
+ let _ = connection
+ .object_server()
+ .remove::<DbusPrompterInterface<P>, String>(path)
+ .await;
+ });
+ }
+}
+
+#[derive(Clone)]
+pub struct DbusHelper {
+ connection: Connection,
+ helper: PolkitHelperProxy<'static>,
+}
+impl DbusHelper {
+ pub async fn new(connection: Connection) -> zbus::Result<Self> {
+ let helper = PolkitHelperProxy::new(&connection).await?;
+ Ok(Self { connection, helper })
+ }
+}
+impl Helper for DbusHelper {
+ async fn help_me<P: Prompter + Send + Sync + 'static>(
+ &self,
+ cookie: &str,
+ prompter: P,
+ identity: Identity,
+ ) -> anyhow::Result<()> {
+ let prompter = TemporaryPrompterInterface::new(self.connection.clone(), prompter).await;
+ self.helper
+ .init_conversation(
+ BackendRequest {
+ cookie: cookie.to_owned(),
+ environment: HashMap::new(),
+ prompter_path: prompter.path.clone(),
+ identity,
+ }, // cookie.to_owned(), HashMap::new(), prompter.path.clone()
+ )
+ .await?;
+ Ok(())
+ }
+}
remowt/cmds/remowt-agent/src/helper/mod.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-agent/src/helper/mod.rs
@@ -0,0 +1,21 @@
+use futures::Future;
+use remowt_polkit_shared::Identity;
+use remowt_ui_prompt::Prompter;
+
+mod dbus;
+mod protocol;
+mod socket;
+mod suid;
+
+pub use dbus::DbusHelper;
+pub use socket::SocketHelper;
+pub use suid::SuidHelper;
+
+pub trait Helper {
+ fn help_me<P: Prompter + Send + Sync + 'static>(
+ &self,
+ cookie: &str,
+ prompt: P,
+ identity: Identity,
+ ) -> impl Future<Output = anyhow::Result<()>> + Send;
+}
remowt/cmds/remowt-agent/src/helper/protocol.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-agent/src/helper/protocol.rs
@@ -0,0 +1,50 @@
+use std::pin::pin;
+
+use anyhow::bail;
+use futures::stream::Peekable;
+use futures::StreamExt as _;
+use remowt_ui_prompt::Prompter;
+use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt as _};
+use tokio::select;
+use tokio_util::codec::{FramedRead, LinesCodec};
+
+pub async fn run_conversation<R, W, P>(reader: R, mut writer: W, prompt: P) -> anyhow::Result<()>
+where
+ R: AsyncRead,
+ W: AsyncWrite + Unpin,
+ P: Prompter,
+{
+ let mut lines = pin!(FramedRead::new(reader, LinesCodec::new()).peekable());
+
+ while let Some(line) = lines.next().await {
+ let line = line?;
+ let res = if let Some(prompt_text) = line.strip_prefix("PAM_PROMPT_ECHO_OFF ") {
+ prompt.prompt_text(false, prompt_text, "", &[]).await?
+ } else if let Some(prompt_text) = line.strip_prefix("PAM_PROMPT_ECHO_ON ") {
+ prompt.prompt_text(true, prompt_text, "", &[]).await?
+ } else if let Some(msg_text) = line.strip_prefix("PAM_ERROR_MSG ") {
+ prompt.display_text(true, msg_text, &[]).await?;
+ String::new()
+ } else if let Some(msg_text) = line.strip_prefix("PAM_TEXT_INFO ") {
+ select! {
+ _ = Peekable::peek(lines.as_mut()) => {},
+ r = prompt.display_text(false, msg_text, &[]) => {r?}
+ }
+ String::new()
+ } else if line == "SUCCESS" {
+ return Ok(());
+ } else if line == "FAILURE" {
+ bail!("helper reported failure")
+ } else {
+ bail!("unknown agent request: {line}")
+ };
+
+ if res.contains('\n') {
+ bail!("response should not include newline")
+ }
+
+ writer.write_all(res.as_bytes()).await?;
+ writer.write_all(b"\n").await?;
+ }
+ bail!("agent finished unexpectedly")
+}
remowt/cmds/remowt-agent/src/helper/socket.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-agent/src/helper/socket.rs
@@ -0,0 +1,53 @@
+use anyhow::{anyhow, bail};
+use nix::unistd::User;
+use remowt_polkit_shared::Identity;
+use remowt_ui_prompt::Prompter;
+use tokio::io::AsyncWriteExt as _;
+use tokio::net::UnixStream;
+use tracing::debug;
+
+use super::protocol::run_conversation;
+use super::Helper;
+
+/// Polkit 127 introduced an alternative backend similar to `lach.PolkitHelper`
+const SOCKET_PATH: &str = "/run/polkit/agent-helper.socket";
+
+#[derive(Clone)]
+pub struct SocketHelper<F> {
+ pub fallback: F,
+}
+
+impl<F: Helper + Sync> Helper for SocketHelper<F> {
+ async fn help_me<P: Prompter + Send + Sync + 'static>(
+ &self,
+ cookie: &str,
+ prompt: P,
+ identity: Identity,
+ ) -> anyhow::Result<()> {
+ let Some(uid) = identity.uid() else {
+ bail!("can't process identity");
+ };
+
+ let stream = match UnixStream::connect(SOCKET_PATH).await {
+ Ok(stream) => stream,
+ Err(e) => {
+ debug!("agent-helper.socket unavailable ({e}), using fallback helper");
+ return self.fallback.help_me(cookie, prompt, identity).await;
+ }
+ };
+
+ let user = User::from_uid(uid)
+ .map_err(|e| anyhow!("error querying user: {e}"))?
+ .ok_or_else(|| anyhow!("user not found"))?;
+
+ assert!(!cookie.contains('\n'));
+ let (reader, mut writer) = stream.into_split();
+
+ writer.write_all(user.name.as_bytes()).await?;
+ writer.write_all(b"\n").await?;
+ writer.write_all(cookie.as_bytes()).await?;
+ writer.write_all(b"\n").await?;
+
+ run_conversation(reader, writer, prompt).await
+ }
+}
remowt/cmds/remowt-agent/src/helper/suid.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-agent/src/helper/suid.rs
@@ -0,0 +1,46 @@
+use std::process::Stdio;
+
+use anyhow::{anyhow, bail};
+use nix::unistd::User;
+use remowt_polkit_shared::Identity;
+use remowt_ui_prompt::Prompter;
+use tokio::io::AsyncWriteExt as _;
+use tokio::process::Command;
+
+use super::protocol::run_conversation;
+use super::Helper;
+
+#[derive(Clone)]
+pub struct SuidHelper;
+impl Helper for SuidHelper {
+ async fn help_me<P: Prompter + 'static>(
+ &self,
+ cookie: &str,
+ prompt: P,
+ identity: Identity,
+ ) -> anyhow::Result<()> {
+ let Some(uid) = identity.uid() else {
+ bail!("can't process identity");
+ };
+ let user = User::from_uid(uid)
+ .map_err(|e| anyhow!("error querying user: {e}"))?
+ .ok_or_else(|| anyhow!("user not found"))?;
+
+ let mut cmd = Command::new("polkit-agent-helper-1");
+ cmd.arg(user.name);
+ cmd.stdin(Stdio::piped());
+ cmd.stdout(Stdio::piped());
+ cmd.kill_on_drop(true);
+ let mut child = cmd.spawn()?;
+ let mut stdin = child.stdin.take().expect("piped");
+ let stdout = child.stdout.take().expect("piped");
+
+ assert!(!cookie.contains('\n'));
+ stdin.write_all(cookie.as_bytes()).await?;
+ stdin.write_all(b"\n").await?;
+
+ let res = run_conversation(stdout, stdin, prompt).await;
+ drop(child);
+ res
+ }
+}
remowt/cmds/remowt-agent/src/main.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-agent/src/main.rs
@@ -0,0 +1,437 @@
+use std::borrow::Cow;
+use std::collections::{BTreeMap, HashMap};
+use std::fs::Permissions;
+use std::future::pending;
+use std::io;
+use std::os::unix::fs::PermissionsExt as _;
+use std::path::PathBuf;
+use std::sync::{Arc, Mutex, OnceLock};
+
+use bifrostlink::declarative::RemoteEndpoints;
+use bifrostlink::Rpc;
+use bifrostlink_ports::stdio::from_stdio;
+use bifrostlink_ports::unix_socket::from_socket;
+use clap::Parser;
+use remowt_endpoints::{
+ forward::Forward, fs::Fs, iroh_tunnel::IrohTunnel, nix_daemon::NixDaemon, pty::Pty,
+ subprocess::Subprocess, systemd::Systemd,
+};
+use remowt_link_shared::iroh_tunnel::TunnelDialer;
+use remowt_link_shared::{editor::EditorEndpointsClient, Address, BifConfig};
+use remowt_polkit_shared::{emphasize, BackendRequest, Identity, PidDisplay};
+use remowt_ui_prompt::bifrost::PromptEndpointsClient;
+use remowt_ui_prompt::rofi::RofiPrompter;
+use remowt_ui_prompt::{PrependSourcePrompter, Prompter, Source};
+use tokio::fs;
+use tokio::net::UnixStream;
+use tokio::runtime::Builder;
+use tokio::task::AbortHandle;
+use tracing::{debug, trace};
+use zbus::fdo;
+use zbus::zvariant::{OwnedValue, Str};
+use zbus::{interface, proxy, Connection};
+use zbus_polkit::policykit1::Subject;
+
+use self::helper::{Helper, SocketHelper, SuidHelper};
+
+pub mod askpass;
+pub mod bus;
+pub mod editor;
+pub mod helper;
+
+struct CancelTaskOnDrop {
+ tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,
+ handle: String,
+}
+impl Drop for CancelTaskOnDrop {
+ fn drop(&mut self) {
+ debug!("cancel on drop");
+ if let Some(task) = self
+ .tasks
+ .lock()
+ .expect("not poisoned")
+ .remove(&self.handle)
+ {
+ task.abort();
+ }
+ }
+}
+
+struct Agent<H, P> {
+ tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,
+ helper: H,
+ prompter: P,
+}
+impl<H, P> Agent<H, P> {
+ fn new(helper: H, prompter: P) -> Self {
+ Agent {
+ tasks: Arc::new(Mutex::new(HashMap::new())),
+ helper,
+ prompter,
+ }
+ }
+}
+
+#[interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]
+impl<H, P> Agent<H, P>
+where
+ H: Helper + Clone + Send + Sync + 'static,
+ P: Prompter + Clone + Send + Sync + 'static,
+{
+ /// BeginAuthentication method
+ #[allow(clippy::too_many_arguments)]
+ async fn begin_authentication(
+ &self,
+ action_id: String,
+ message: String,
+ _icon_name: String,
+ mut details: BTreeMap<String, String>,
+ cookie: String,
+ identities: Vec<Identity>,
+ ) -> zbus::fdo::Result<()> {
+ use std::fmt::Write;
+ debug!("begin auth");
+ let _cancel_guard = Arc::new(OnceLock::new());
+ let task = {
+ let helper = self.helper.clone();
+ let prompter = self.prompter.clone();
+ let cookie = cookie.clone();
+ let _cancel_guard = _cancel_guard.clone();
+ tokio::task::spawn(async move {
+ let _cancel_guard = _cancel_guard.clone();
+ trace!("conversation task");
+ let mut description = format!("{message}\n\n<b>Action id:</b> {action_id}",);
+ if let Some(subject) = details.remove("polkit.caller-pid") {
+ let _ = write!(description, "\n<b>Caller:</b> ");
+ if let Ok(pid) = subject.parse::<u32>() {
+ let _ = write!(description, "{}", PidDisplay(pid));
+ } else {
+ let _ = write!(description, "{}", emphasize("invalid pid"));
+ }
+ }
+ if let Some(subject) = details.remove("polkit.subject-pid") {
+ let _ = write!(description, "\n<b>Subject:</b> ");
+ if let Ok(pid) = subject.parse::<u32>() {
+ let _ = write!(description, "{}", PidDisplay(pid));
+ } else {
+ let _ = write!(description, "{}", emphasize("invalid pid"));
+ }
+ }
+ let mut prompter = PrependSourcePrompter {
+ source: vec![Source(Cow::Borrowed("polkit agent"))],
+ description: description.clone(),
+ prompter,
+ };
+
+ let identity_displays: Vec<String> =
+ identities.iter().map(|v| v.to_string()).collect();
+ let identity_displays: Vec<&str> =
+ identity_displays.iter().map(|v| v.as_str()).collect();
+ debug!("choose identity");
+ let choosen_identity = match identity_displays.len() {
+ 0 => {
+ return Err(fdo::Error::AuthFailed(
+ "no identity to authenticate as".to_owned(),
+ ))
+ }
+ 1 => 0,
+ _ => {
+ prompter
+ .prompt_enum(
+ "Identity",
+ "Select identity to use for polkit authorization",
+ &identity_displays,
+ &[],
+ )
+ .await?
+ }
+ };
+ debug!("identity chosen");
+
+ let _ = write!(
+ description,
+ "\n<b>Identity:</b> {}",
+ identities[choosen_identity as usize]
+ );
+ prompter.description = description;
+
+ prompter.source.push(Source(Cow::Borrowed("polkit daemon")));
+
+ helper
+ .help_me(
+ &cookie,
+ prompter,
+ identities[choosen_identity as usize].clone(),
+ )
+ .await
+ .map_err(|e| fdo::Error::Failed(e.to_string()))?;
+ // let connection = Connection::system().await?;
+ // let helper = PolkitHelperProxy::new(&connection).await?;
+
+ Ok(())
+ })
+ };
+ self.tasks
+ .lock()
+ .unwrap()
+ .insert(cookie.clone(), task.abort_handle());
+ debug!("abort handle stored");
+ let _ = _cancel_guard.set(CancelTaskOnDrop {
+ tasks: self.tasks.clone(),
+ handle: cookie.clone(),
+ });
+
+ let _ = task.await;
+
+ Ok(())
+ }
+
+ /// CancelAuthentication method
+ async fn cancel_authentication(&self, cookie: &str) -> zbus::fdo::Result<()> {
+ debug!("auth cancelled");
+ if let Some(abort) = self.tasks.lock().unwrap().remove(cookie) {
+ debug!("abort handle found");
+ abort.abort();
+ }
+ // debug!("Authentication cancled ! {cookie}");
+ Ok(())
+ }
+}
+
+const OBJ_PATH: &str = "/org/freedesktop/PolicyKit1/AuthenticationAgent";
+
+#[proxy(
+ interface = "lach.PolkitHelper",
+ default_service = "lach.polkit.helper1",
+ default_path = "/lach/PolkitHelper"
+)]
+trait PolkitHelper {
+ fn init_conversation(&self, request: BackendRequest) -> zbus::Result<()>;
+}
+
+#[derive(Parser)]
+enum Opts {
+ AskPass {
+ prompt: String,
+ description: String,
+ },
+ Editor {
+ /// Argument to nvim
+ path: String,
+ },
+ /// Expose a remote TCP/UDP address on the local (User) machine, printing the local port.
+ Forward {
+ /// Protocol: `tcp` (ssh or iroh) or `udp` (iroh-only).
+ proto: ForwardProto,
+ /// Remote socket address to expose locally, e.g. `127.0.0.1:8080`.
+ addr: String,
+ },
+ RealAgent {
+ #[arg(long)]
+ path: Option<PathBuf>,
+ /// Expect own address to be AgentPrivileged, skip installing polkit agent
+ #[arg(long)]
+ privileged: bool,
+ #[arg(long)]
+ local: bool,
+ },
+ LocalAgent,
+}
+
+#[derive(Clone, Copy, clap::ValueEnum)]
+enum ForwardProto {
+ Tcp,
+ Udp,
+}
+
+fn main() -> anyhow::Result<()> {
+ tracing_subscriber::fmt()
+ .with_writer(io::stderr)
+ .without_time()
+ .init();
+ let opts = Opts::parse();
+
+ let runtime = Builder::new_current_thread().enable_all().build()?;
+
+ match opts {
+ Opts::AskPass {
+ prompt,
+ description,
+ } => runtime.block_on(askpass::ask(&prompt, description)),
+ Opts::LocalAgent => runtime.block_on(main_real()),
+ Opts::Editor { path } => runtime.block_on(editor::edit(path)),
+ Opts::Forward { proto, addr } => {
+ runtime.block_on(editor::forward(matches!(proto, ForwardProto::Udp), addr))
+ }
+ Opts::RealAgent {
+ path,
+ privileged,
+ local,
+ } => runtime.block_on(main_real_agent(path, privileged, local)),
+ }
+}
+async fn main_real() -> anyhow::Result<()> {
+ let system_conn = Connection::system().await?;
+ let helper = SocketHelper {
+ fallback: SuidHelper,
+ };
+ register_auth_agent(&system_conn, Agent::new(helper, RofiPrompter)).await?;
+
+ let session_conn = Connection::session().await?;
+ askpass::serve(&session_conn, RofiPrompter).await?;
+
+ let _keep_alive = (system_conn, session_conn);
+ pending().await
+}
+async fn main_real_agent(
+ path: Option<PathBuf>,
+ privileged: bool,
+ local: bool,
+) -> anyhow::Result<()> {
+ let address = if privileged {
+ Address::AgentPrivileged
+ } else {
+ Address::Agent
+ };
+ let mut rpc = Rpc::<BifConfig>::new(address);
+
+ let dialer = Arc::new(TunnelDialer::new());
+ Fs::new().register_endpoints(&mut rpc);
+ Systemd.register_endpoints(&mut rpc);
+ Pty::new(dialer.clone()).register_endpoints(&mut rpc);
+ Subprocess::new(dialer.clone()).register_endpoints(&mut rpc);
+ NixDaemon::new(dialer.clone()).register_endpoints(&mut rpc);
+ IrohTunnel::new(dialer.clone()).register_endpoints(&mut rpc);
+ Forward::new(dialer.clone()).register_endpoints(&mut rpc);
+
+ remowt_plugin::host::serve(&mut rpc);
+
+ let user_prompter = PromptEndpointsClient::wrap(rpc.remote(Address::User));
+ let editor_client = EditorEndpointsClient::wrap(rpc.remote(Address::User));
+
+ let bus = bus::spawn().await?;
+ askpass::serve(&bus.conn, user_prompter.clone()).await?;
+ editor::serve(&bus.conn, editor_client).await?;
+
+ let helpers = tempfile::Builder::new().prefix("remowt-path.").tempdir()?;
+ let exe = std::env::current_exe()?;
+ let askpass_helper = helpers.path().join("remowt-askpass");
+ let editor_helper = helpers.path().join("remowt-editor");
+ let forward_helper = helpers.path().join("remowt-forward");
+ {
+ let script = format!(
+ "#!/bin/sh\nexec {} ask-pass \"password\" \"$1\"\n",
+ sh_quote(&exe.to_string_lossy())
+ );
+ fs::write(&askpass_helper, script).await?;
+ fs::set_permissions(&askpass_helper, Permissions::from_mode(0o755)).await?;
+ }
+ {
+ let script = format!(
+ "#!/bin/sh\nexec {} editor \"$1\"\n",
+ sh_quote(&exe.to_string_lossy())
+ );
+ fs::write(&editor_helper, script).await?;
+ fs::set_permissions(&editor_helper, Permissions::from_mode(0o755)).await?;
+ }
+ {
+ let script = format!(
+ "#!/bin/sh\nexec {} forward \"$@\"\n",
+ sh_quote(&exe.to_string_lossy())
+ );
+ fs::write(&forward_helper, script).await?;
+ fs::set_permissions(&forward_helper, Permissions::from_mode(0o755)).await?;
+ }
+
+ // Safety: Hoping tokio own threads won't read any of those...
+ unsafe {
+ prepend_path(helpers.path());
+ std::env::set_var("SUDO_ASKPASS", &askpass_helper);
+ std::env::set_var("SSH_ASKPASS", &askpass_helper);
+ std::env::set_var("SSH_ASKPASS_REQUIRE", "force");
+ std::env::set_var("EDITOR", &editor_helper);
+ std::env::set_var("VISUAL", &editor_helper);
+ std::env::set_var("DBUS_SESSION_BUS_ADDRESS", &bus.address);
+ }
+
+ let port = match path {
+ Some(path) => from_socket(UnixStream::connect(path).await?),
+ None => from_stdio(),
+ };
+ rpc.add_direct(Address::User, port, bifrostlink::Rtt(0));
+
+ let polkit_conn = if !privileged && !local {
+ // The unprivileged agent doubles as a polkit authentication agent so
+ // `run0` (e.g. our own elevation) routes its prompt to the User over
+ // bifrost instead of failing on a tty-less session.
+ let conn = Connection::system().await?;
+ let helper = SocketHelper {
+ fallback: SuidHelper,
+ };
+ register_auth_agent(&conn, Agent::new(helper, user_prompter)).await?;
+ Some(conn)
+ } else {
+ None
+ };
+
+ let _keep_alive = (bus, helpers, polkit_conn);
+ pending().await
+}
+
+async fn register_auth_agent<H, P>(conn: &Connection, agent: Agent<H, P>) -> anyhow::Result<()>
+where
+ H: Helper + Clone + Send + Sync + 'static,
+ P: Prompter + Clone + Send + Sync + 'static,
+{
+ let proxy = zbus_polkit::policykit1::AuthorityProxy::new(conn).await?;
+ conn.object_server().at(OBJ_PATH, agent).await?;
+
+ let subject = auth_agent_subject()?;
+ proxy
+ .register_authentication_agent(&subject, "C", OBJ_PATH)
+ .await?;
+ debug!(kind = subject.subject_kind, "registered polkit agent");
+ Ok(())
+}
+
+fn auth_agent_subject() -> anyhow::Result<Subject> {
+ let mut details = HashMap::new();
+ if let Ok(session_id) = std::env::var("XDG_SESSION_ID") {
+ let val: OwnedValue = Str::from(session_id).into();
+ details.insert("session-id".to_string(), val);
+ return Ok(Subject {
+ subject_kind: "unix-session".to_string(),
+ subject_details: details,
+ });
+ }
+
+ details.insert("pid".to_string(), OwnedValue::from(std::process::id()));
+ Ok(Subject {
+ subject_kind: "unix-process".to_string(),
+ subject_details: details,
+ })
+}
+
+fn sh_quote(s: &str) -> String {
+ format!("'{}'", s.replace('\'', "'\\''"))
+}
+
+/// Prepend `dir` to the process `PATH`.
+///
+/// # SAFETY
+///
+/// Same as `set_var`
+unsafe fn prepend_path(dir: &std::path::Path) {
+ let value = match std::env::var_os("PATH") {
+ Some(existing) => {
+ let mut v = dir.as_os_str().to_owned();
+ v.push(":");
+ v.push(existing);
+ v
+ }
+ None => dir.as_os_str().to_owned(),
+ };
+ unsafe {
+ std::env::set_var("PATH", value);
+ }
+}
remowt/cmds/remowt-ssh/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-ssh/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "remowt-ssh"
+description = "SSH transport client for connecting to a remowt agent"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+clap = { workspace = true, features = ["derive"] }
+tracing-subscriber.workspace = true
+remowt-link-shared.workspace = true
+remowt-client.workspace = true
+tokio = { workspace = true, features = [
+ "macros",
+ "fs",
+ "net",
+ "io-util",
+ "rt",
+ "signal",
+] }
+nix = { workspace = true, features = ["term"] }
+anyhow.workspace = true
+tracing.workspace = true
+remowt-ui-prompt.workspace = true
remowt/cmds/remowt-ssh/src/main.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/cmds/remowt-ssh/src/main.rs
@@ -0,0 +1,230 @@
+use std::borrow::Cow;
+use std::env::VarError;
+use std::io;
+use std::os::fd::{AsRawFd, RawFd};
+use std::path::PathBuf;
+use std::pin::Pin;
+use std::task::{Context, Poll};
+
+use anyhow::anyhow;
+use clap::Parser;
+use nix::libc;
+use nix::sys::termios::{self, SetArg, Termios};
+use remowt_client::editor::SshEditor;
+use remowt_client::{AgentBundle, Remowt};
+use remowt_link_shared::editor::serve_editor;
+use remowt_ui_prompt::auto::AutoPrompter;
+use remowt_ui_prompt::bifrost::serve_prompts;
+use remowt_ui_prompt::{PrependSourcePrompter, Source};
+use tokio::io::unix::AsyncFd;
+use tokio::io::{AsyncRead, ReadBuf};
+use tokio::signal::unix::{signal, SignalKind};
+use tracing::debug;
+
+#[derive(Parser)]
+enum Opts {
+ /// Connect to remote host with remowt agent.
+ Ssh {
+ host: String,
+ #[arg(long)]
+ escalate: bool,
+ },
+ /// Connect to local host for testing the connectivity.
+ Local {
+ #[arg(long)]
+ escalate: bool,
+ },
+}
+
+fn agents_dir() -> anyhow::Result<PathBuf> {
+ std::env::var_os("REMOWT_AGENTS_DIR")
+ .map(PathBuf::from)
+ .or_else(|| option_env!("REMOWT_AGENTS_DIR").map(PathBuf::from))
+ .ok_or_else(|| anyhow!("no remowt-agents bundle"))
+}
+
+#[tokio::main(flavor = "current_thread")]
+async fn main() -> anyhow::Result<()> {
+ tracing_subscriber::fmt()
+ .with_writer(std::io::stderr)
+ .without_time()
+ .init();
+ let opts = Opts::parse();
+
+ let bundle = AgentBundle::from_dir(agents_dir()?)?;
+ let (conn, escalate) = match &opts {
+ Opts::Ssh { host, escalate } => (
+ Remowt::connect(host, &bundle, "remowt-ssh".to_owned()).await?,
+ *escalate,
+ ),
+ Opts::Local { escalate } => (
+ Remowt::connect_local(&bundle, "remowt-ssh".to_owned()).await?,
+ *escalate,
+ ),
+ };
+ let mut rpc = conn.rpc();
+
+ serve_prompts(
+ &mut rpc,
+ PrependSourcePrompter {
+ prompter: AutoPrompter::new().await,
+ source: match opts {
+ Opts::Ssh { host, .. } => vec![Source(Cow::Owned(format!("ssh host: {}", host)))],
+ Opts::Local { .. } => vec![],
+ },
+ description: "".to_owned(),
+ },
+ );
+ if let Some(sess) = conn.ssh() {
+ serve_editor(
+ &mut rpc,
+ SshEditor {
+ sess,
+ conn: conn.clone(),
+ },
+ );
+ }
+
+ debug!("entering shell");
+ run_shell(&conn, escalate).await?;
+ debug!("shell ended");
+
+ Ok(())
+}
+
+async fn run_shell(conn: &Remowt, escalate: bool) -> anyhow::Result<()> {
+ let term = match std::env::var("TERM") {
+ Ok(v) => v,
+ Err(VarError::NotPresent) => "xterm-256color".to_owned(),
+ Err(e) => return Err(e.into()),
+ };
+ let (cols, rows) = term_size().unwrap_or((80, 24));
+
+ let shell = conn.open_shell(&term, cols, rows, escalate).await?;
+ let resizer = shell.resizer();
+ let stream = shell.stream;
+
+ let _raw = RawMode::enable();
+
+ if let Ok(mut winch) = signal(SignalKind::window_change()) {
+ tokio::spawn(async move {
+ while winch.recv().await.is_some() {
+ if let Some((cols, rows)) = term_size() {
+ let _ = resizer.resize(cols, rows).await;
+ }
+ }
+ });
+ }
+
+ let (mut from_remote, mut to_remote) = tokio::io::split(stream);
+ let mut stdin = AsyncStdin::new()?;
+ let mut stdout = tokio::io::stdout();
+
+ tokio::select! {
+ r = tokio::io::copy(&mut from_remote, &mut stdout) => { r?; }
+ _ = tokio::io::copy(&mut stdin, &mut to_remote) => {}
+ }
+
+ Ok(())
+}
+
+struct AsyncStdin {
+ fd: AsyncFd<RawFd>,
+ original_flags: i32,
+}
+
+impl AsyncStdin {
+ fn new() -> io::Result<Self> {
+ let raw = libc::STDIN_FILENO;
+ // SAFETY: F_GETFL/F_SETFL round-trip on a valid fd.
+ let original_flags = unsafe { libc::fcntl(raw, libc::F_GETFL) };
+ if original_flags < 0 {
+ return Err(io::Error::last_os_error());
+ }
+ if unsafe { libc::fcntl(raw, libc::F_SETFL, original_flags | libc::O_NONBLOCK) } < 0 {
+ return Err(io::Error::last_os_error());
+ }
+ Ok(Self {
+ fd: AsyncFd::new(raw)?,
+ original_flags,
+ })
+ }
+}
+
+impl Drop for AsyncStdin {
+ fn drop(&mut self) {
+ // SAFETY: restoring the flags we saved on a valid fd.
+ unsafe { libc::fcntl(libc::STDIN_FILENO, libc::F_SETFL, self.original_flags) };
+ }
+}
+
+impl AsyncRead for AsyncStdin {
+ fn poll_read(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &mut ReadBuf<'_>,
+ ) -> Poll<io::Result<()>> {
+ let this = self.get_mut();
+ loop {
+ let mut guard = match this.fd.poll_read_ready(cx) {
+ Poll::Ready(Ok(g)) => g,
+ Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
+ Poll::Pending => return Poll::Pending,
+ };
+ let unfilled = buf.initialize_unfilled();
+ let res = guard.try_io(|inner| {
+ let fd = *inner.get_ref();
+ // SAFETY: writing into `unfilled`'s own backing storage.
+ let n = unsafe { libc::read(fd, unfilled.as_mut_ptr().cast(), unfilled.len()) };
+ if n < 0 {
+ Err(io::Error::last_os_error())
+ } else {
+ Ok(n as usize)
+ }
+ });
+ match res {
+ Ok(Ok(n)) => {
+ buf.advance(n);
+ return Poll::Ready(Ok(()));
+ }
+ Ok(Err(e)) => return Poll::Ready(Err(e)),
+ Err(_would_block) => continue,
+ }
+ }
+ }
+}
+
+fn term_size() -> Option<(u16, u16)> {
+ let mut ws: libc::winsize = unsafe { std::mem::zeroed() };
+ let rc = unsafe { libc::ioctl(libc::STDIN_FILENO, libc::TIOCGWINSZ, &mut ws) };
+ if rc != 0 || ws.ws_col == 0 {
+ None
+ } else {
+ Some((ws.ws_col, ws.ws_row))
+ }
+}
+
+struct RawMode {
+ original: Termios,
+}
+
+impl RawMode {
+ fn enable() -> Option<Self> {
+ let stdin = std::io::stdin();
+ // SAFETY: trivial libc call on a borrowed fd.
+ if unsafe { libc::isatty(stdin.as_raw_fd()) } != 1 {
+ return None;
+ }
+ let original = termios::tcgetattr(&stdin).ok()?;
+ let mut raw = original.clone();
+ termios::cfmakeraw(&mut raw);
+ termios::tcsetattr(&stdin, SetArg::TCSANOW, &raw).ok()?;
+ Some(Self { original })
+ }
+}
+
+impl Drop for RawMode {
+ fn drop(&mut self) {
+ let _ = termios::tcsetattr(std::io::stdin(), SetArg::TCSANOW, &self.original);
+ }
+}
remowt/crates/polkit-shared/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/polkit-shared/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "remowt-polkit-shared"
+description = "Shared polkit/PAM types for remowt"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+nix = { workspace = true, features = ["user"] }
+serde = { workspace = true, features = ["derive"] }
+zbus.workspace = true
remowt/crates/polkit-shared/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/polkit-shared/src/lib.rs
@@ -0,0 +1,113 @@
+use std::collections::HashMap;
+use std::{fmt, fs};
+
+use nix::unistd::{Uid, User};
+use serde::{Deserialize, Serialize};
+use zbus::zvariant::{OwnedValue, Type, Value};
+
+pub fn emphasize(s: impl AsRef<str>) -> String {
+ format!("<span style=\"italic\"><{}></span>", escape(s),)
+}
+fn command(s: impl AsRef<str>) -> String {
+ format!("<u><tt>{}</tt></u>", s.as_ref())
+}
+fn escape(s: impl AsRef<str>) -> String {
+ s.as_ref()
+ .replace("&", """)
+ .replace("<", "<")
+ .replace(">", ">")
+}
+
+pub struct PidDisplay(pub u32);
+impl fmt::Display for PidDisplay {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if self.0 == 1 {
+ emphasize("init").fmt(f)
+ } else if let Ok(proc) = fs::read_to_string(format!("/proc/{}/cmdline", self.0)) {
+ write!(
+ f,
+ "command: {}",
+ command(
+ proc.replace("\0", " ")
+ .strip_suffix(" ")
+ .expect("cmdline should end with NUL")
+ )
+ )
+ } else if let Ok(proc) = fs::read_to_string(format!("/proc/{}/comm", self.0)) {
+ write!(f, "process: {}", command(proc.replace("\0", " ")))
+ } else {
+ emphasize("unknown process").fmt(f)
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize, Type, PartialEq, Debug)]
+pub struct Identity {
+ pub kind: String,
+ pub details: HashMap<String, OwnedValue>,
+}
+
+impl Identity {
+ pub fn uid(&self) -> Option<Uid> {
+ if self.kind != "unix-user" {
+ return None;
+ }
+ let uid = self.details.get("uid")?;
+ let Value::U32(uid) = &**uid else {
+ return None;
+ };
+ Some(Uid::from_raw(*uid))
+ }
+}
+
+impl fmt::Display for Identity {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self.kind.as_str() {
+ "unix-user" => match self.details.get("uid").map(|v| &**v) {
+ Some(Value::U32(uid)) => match User::from_uid(Uid::from_raw(*uid)) {
+ Ok(Some(u)) => write!(
+ f,
+ "user: {} {} {}",
+ u.name,
+ u.uid,
+ if u.gecos.is_empty() {
+ "".to_owned()
+ } else {
+ format!(": {}", escape(u.gecos.to_string_lossy()))
+ }
+ ),
+ Ok(None) => emphasize("not found").fmt(f),
+ Err(e) => {
+ let user = format!("could not get user: {e}");
+ emphasize(&user).fmt(f)?;
+ Ok(())
+ }
+ },
+
+ _ => emphasize("unknown uid").fmt(f),
+ },
+ _ => emphasize(format!("identity of unknown kind: {}", self.kind)).fmt(f),
+ }
+ }
+}
+
+impl Clone for Identity {
+ fn clone(&self) -> Self {
+ Self {
+ kind: self.kind.clone(),
+ details: self
+ .details
+ .iter()
+ .map(|(k, v)| (k.clone(), v.try_clone().expect("no fds are expected")))
+ .collect(),
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize, Type, PartialEq, Debug)]
+pub struct BackendRequest {
+ pub cookie: String,
+ pub environment: HashMap<String, String>,
+ pub prompter_path: String,
+ pub identity: Identity,
+}
remowt/crates/remowt-client/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-client/Cargo.toml
@@ -0,0 +1,32 @@
+[package]
+name = "remowt-client"
+description = "russh-based client connection to a remowt agent"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+bifrostlink.workspace = true
+bytes.workspace = true
+camino.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+remowt-link-shared.workspace = true
+russh.workspace = true
+russh-config.workspace = true
+tempfile.workspace = true
+tokio = { workspace = true, features = [
+ "net",
+ "io-util",
+ "rt",
+ "sync",
+ "macros",
+ "process",
+] }
+tracing.workspace = true
+uuid = { workspace = true, features = ["v4"] }
+remowt-endpoints.workspace = true
+tokio-util = { workspace = true, features = ["codec"] }
+futures.workspace = true
+iroh.workspace = true
remowt/crates/remowt-client/src/editor.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-client/src/editor.rs
@@ -0,0 +1,183 @@
+use std::collections::HashMap;
+use std::net::SocketAddr;
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::{Arc, Mutex};
+
+use remowt_endpoints::forward::ForwardClient;
+use remowt_link_shared::editor::{EditorBackend, Error};
+use remowt_link_shared::BifConfig;
+use russh::client::Handle;
+use tokio::net::{TcpListener, UdpSocket, UnixListener};
+use tracing::error;
+
+use crate::{Remowt, SshHandler};
+
+pub struct SshEditor {
+ pub sess: Arc<Handle<SshHandler>>,
+ pub conn: Remowt,
+}
+impl EditorBackend for SshEditor {
+ async fn open_editor(&self, socket_path: String) -> Result<(), Error> {
+ let local = std::env::temp_dir().join(format!("remowt-nvim-{}.sock", uuid::Uuid::new_v4()));
+ let _ = std::fs::remove_file(&local);
+ let listener = UnixListener::bind(&local).map_err(|e| Error::Failed(e.to_string()))?;
+
+ let sess = self.sess.clone();
+ let forward = tokio::spawn(async move {
+ loop {
+ let Ok((mut stream, _)) = listener.accept().await else {
+ break;
+ };
+ let sess = sess.clone();
+ let remote = socket_path.clone();
+ tokio::spawn(async move {
+ match sess.channel_open_direct_streamlocal(remote).await {
+ Ok(ch) => {
+ let mut remote = ch.into_stream();
+ let _ = tokio::io::copy_bidirectional(&mut stream, &mut remote).await;
+ }
+ Err(e) => error!("opening direct-streamlocal to nvim failed: {e}"),
+ }
+ });
+ }
+ });
+
+ let status = tokio::process::Command::new("neovide")
+ .arg("--no-fork")
+ .arg("--server")
+ .arg(&local)
+ .status()
+ .await
+ .map_err(|e| Error::Failed(format!("spawning neovide: {e}")));
+
+ forward.abort();
+ let _ = std::fs::remove_file(&local);
+
+ match status? {
+ s if s.success() => Ok(()),
+ s => Err(Error::Failed(format!("neovide exited with {s}"))),
+ }
+ }
+
+ async fn expose_tcp(&self, addr: String) -> Result<u16, Error> {
+ let listener = TcpListener::bind(("127.0.0.1", 0))
+ .await
+ .map_err(|e| Error::Failed(e.to_string()))?;
+ let local = listener
+ .local_addr()
+ .map_err(|e| Error::Failed(e.to_string()))?
+ .port();
+
+ let conn = self.conn.clone();
+ tokio::spawn(async move {
+ loop {
+ let Ok((mut tcp, _)) = listener.accept().await else {
+ break;
+ };
+ let conn = conn.clone();
+ let addr = addr.clone();
+ tokio::spawn(async move {
+ let (forwarded, tunnel) = match conn.bind_fast_tunnel("forward", false).await {
+ Ok(v) => v,
+ Err(e) => {
+ error!("forward: bind tunnel failed: {e}");
+ return;
+ }
+ };
+ let fclient: ForwardClient<BifConfig> = conn.endpoints();
+ match fclient.connect_tcp(tunnel, addr).await {
+ Ok(Ok(())) => {}
+ Ok(Err(e)) => {
+ error!("forward: agent connect_tcp failed: {e}");
+ return;
+ }
+ Err(e) => {
+ error!("forward: connect_tcp rpc failed: {e}");
+ return;
+ }
+ }
+ match forwarded.accept().await {
+ Ok(mut stream) => {
+ let _ = tokio::io::copy_bidirectional(&mut tcp, &mut stream).await;
+ }
+ Err(e) => error!("forward: accept tunnel failed: {e}"),
+ }
+ });
+ }
+ });
+
+ Ok(local)
+ }
+
+ async fn expose_udp(&self, addr: String) -> Result<u16, Error> {
+ let router = self.conn.datagram_router().ok_or_else(|| {
+ Error::Failed(
+ "udp forward requires the iroh fast path, which is not established".into(),
+ )
+ })?;
+
+ let fclient: ForwardClient<BifConfig> = self.conn.endpoints();
+ let session = fclient
+ .open_udp(addr)
+ .await
+ .map_err(|e| Error::Failed(format!("open_udp rpc: {e}")))?
+ .map_err(|e| Error::Failed(format!("agent open_udp: {e}")))?;
+
+ let sock = Arc::new(
+ UdpSocket::bind(("127.0.0.1", 0))
+ .await
+ .map_err(|e| Error::Failed(e.to_string()))?,
+ );
+ let local = sock
+ .local_addr()
+ .map_err(|e| Error::Failed(e.to_string()))?
+ .port();
+
+ let sub_for_source: Arc<Mutex<HashMap<SocketAddr, u64>>> =
+ Arc::new(Mutex::new(HashMap::new()));
+ let source_for_sub: Arc<Mutex<HashMap<u64, SocketAddr>>> =
+ Arc::new(Mutex::new(HashMap::new()));
+ let next_sub = Arc::new(AtomicU64::new(0));
+ let mut rx = router.register(session);
+
+ let up_sock = sock.clone();
+ let up_router = router.clone();
+ let down_source_for_sub = source_for_sub.clone();
+ tokio::spawn(async move {
+ let mut buf = vec![0u8; 65535];
+ loop {
+ let (n, src) = match up_sock.recv_from(&mut buf).await {
+ Ok(v) => v,
+ Err(_) => break,
+ };
+ let sub = {
+ let mut by_src = sub_for_source.lock().expect("lock");
+ if let Some(&sub) = by_src.get(&src) {
+ sub
+ } else {
+ let sub = next_sub.fetch_add(1, Ordering::Relaxed);
+ by_src.insert(src, sub);
+ source_for_sub.lock().expect("lock").insert(sub, src);
+ sub
+ }
+ };
+ if up_router.send(session, sub, &buf[..n]).is_err() {
+ break;
+ }
+ }
+ up_router.unregister(session);
+ });
+
+ let down_sock = sock.clone();
+ tokio::spawn(async move {
+ while let Some((sub, payload)) = rx.recv().await {
+ let dst = down_source_for_sub.lock().expect("lock").get(&sub).copied();
+ if let Some(dst) = dst {
+ let _ = down_sock.send_to(&payload, dst).await;
+ }
+ }
+ });
+
+ Ok(local)
+ }
+}
remowt/crates/remowt-client/src/forwarded.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-client/src/forwarded.rs
@@ -0,0 +1,89 @@
+use std::io;
+use std::pin::Pin;
+use std::task::{Context, Poll};
+
+use anyhow::{anyhow, Result};
+use camino::Utf8PathBuf;
+use remowt_link_shared::iroh_tunnel::IrohBiStream;
+use russh::client::Msg;
+use russh::{Channel, ChannelStream};
+use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
+use tokio::net::{UnixListener, UnixStream};
+use tokio::sync::oneshot;
+
+pub enum RemowtListener {
+ Ssh(oneshot::Receiver<Channel<Msg>>),
+ Local(UnixListener, Utf8PathBuf),
+ Iroh(oneshot::Receiver<RemowtStream>),
+}
+
+impl RemowtListener {
+ pub async fn accept(self) -> Result<RemowtStream> {
+ match self {
+ RemowtListener::Ssh(rx) => {
+ let ch = rx
+ .await
+ .map_err(|_| anyhow!("agent never connected the forwarded socket"))?;
+ Ok(RemowtStream::Ssh(ch.into_stream()))
+ }
+ RemowtListener::Local(listener, path) => {
+ let (stream, _) = listener.accept().await?;
+ let _ = std::fs::remove_file(&path);
+ Ok(RemowtStream::Local(stream))
+ }
+ RemowtListener::Iroh(rx) => rx
+ .await
+ .map_err(|_| anyhow!("agent never opened the iroh tunnel")),
+ }
+ }
+}
+
+pub enum RemowtStream {
+ Ssh(ChannelStream<Msg>),
+ Local(UnixStream),
+ Iroh(IrohBiStream),
+}
+
+impl AsyncRead for RemowtStream {
+ fn poll_read(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &mut ReadBuf<'_>,
+ ) -> Poll<io::Result<()>> {
+ match self.get_mut() {
+ RemowtStream::Ssh(s) => Pin::new(s).poll_read(cx, buf),
+ RemowtStream::Local(s) => Pin::new(s).poll_read(cx, buf),
+ RemowtStream::Iroh(s) => Pin::new(s).poll_read(cx, buf),
+ }
+ }
+}
+
+impl AsyncWrite for RemowtStream {
+ fn poll_write(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &[u8],
+ ) -> Poll<io::Result<usize>> {
+ match self.get_mut() {
+ RemowtStream::Ssh(s) => Pin::new(s).poll_write(cx, buf),
+ RemowtStream::Local(s) => Pin::new(s).poll_write(cx, buf),
+ RemowtStream::Iroh(s) => Pin::new(s).poll_write(cx, buf),
+ }
+ }
+
+ fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ match self.get_mut() {
+ RemowtStream::Ssh(s) => Pin::new(s).poll_flush(cx),
+ RemowtStream::Local(s) => Pin::new(s).poll_flush(cx),
+ RemowtStream::Iroh(s) => Pin::new(s).poll_flush(cx),
+ }
+ }
+
+ fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ match self.get_mut() {
+ RemowtStream::Ssh(s) => Pin::new(s).poll_shutdown(cx),
+ RemowtStream::Local(s) => Pin::new(s).poll_shutdown(cx),
+ RemowtStream::Iroh(s) => Pin::new(s).poll_shutdown(cx),
+ }
+ }
+}
remowt/crates/remowt-client/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-client/src/lib.rs
@@ -0,0 +1,714 @@
+use std::collections::HashMap;
+use std::env;
+use std::path::PathBuf;
+use std::sync::atomic::AtomicU64;
+use std::sync::{Arc, Mutex};
+
+use anyhow::{anyhow, bail, ensure, Context as _, Result};
+use bifrostlink::declarative::RemoteEndpoints;
+use bifrostlink::{Remote, Rpc, Rtt};
+use camino::{Utf8Path, Utf8PathBuf};
+use remowt_link_shared::iroh_tunnel::{DatagramRouter, IrohBiStream, TunnelAddr};
+use remowt_link_shared::plugin::PluginEndpointsClient;
+use remowt_link_shared::port::child_port;
+use remowt_link_shared::{Address, BifConfig};
+use russh::client::{connect, Config, Handle, Handler, Msg, Session};
+use russh::keys::agent::client::AgentClient;
+use russh::keys::agent::AgentIdentity;
+use russh::keys::check_known_hosts;
+use russh::keys::ssh_key::PublicKey;
+use russh::Channel;
+use tempfile::TempDir;
+use tokio::io::AsyncRead;
+use tokio::net::UnixListener;
+use tokio::sync::oneshot;
+use tokio::task::JoinHandle;
+use tokio::{
+ fs,
+ io::{AsyncBufReadExt as _, AsyncReadExt as _, AsyncWriteExt as _, BufReader},
+};
+use tracing::{debug, info, warn};
+use uuid::Uuid;
+
+pub mod editor;
+mod forwarded;
+mod shell;
+mod ssh_exec;
+mod subprocess;
+
+use self::ssh_exec::SshExecChild;
+pub use self::subprocess::{RemowtChild, SpawnOptions, StderrMode, StdioMode};
+pub use forwarded::{RemowtListener, RemowtStream};
+pub use shell::{RemowtShell, RemowtShellResizer};
+
+type Subs = Arc<Mutex<HashMap<Utf8PathBuf, oneshot::Sender<Channel<Msg>>>>>;
+
+fn sh_quote(s: impl AsRef<str>) -> String {
+ format!("'{}'", s.as_ref().replace('\'', "'\\''"))
+}
+
+const ESCALATORS: [(&str, &[&str]); 2] = [("run0", &["--background=", "--pipe"]), ("sudo", &[])];
+
+pub struct AgentBundle {
+ dir: PathBuf,
+ hashes: HashMap<String, String>,
+}
+
+impl AgentBundle {
+ pub fn from_dir(dir: impl Into<PathBuf>) -> Result<Self> {
+ let dir = dir.into();
+ let hashes_path = dir.join("hashes");
+ let raw = std::fs::read_to_string(&hashes_path)
+ .with_context(|| format!("reading agent hashes at {}", hashes_path.display()))?;
+ let mut hashes = HashMap::new();
+ for line in raw.lines() {
+ let line = line.trim();
+ if line.is_empty() {
+ continue;
+ }
+ let (arch, hash) = line
+ .split_once(char::is_whitespace)
+ .ok_or_else(|| anyhow!("malformed hashes line: {line:?}"))?;
+ hashes.insert(arch.to_owned(), hash.trim().to_owned());
+ }
+ ensure!(
+ !hashes.is_empty(),
+ "agent bundle {} has no hashes",
+ dir.display()
+ );
+ Ok(Self { dir, hashes })
+ }
+
+ fn binary(&self, arch: &str) -> PathBuf {
+ self.dir.join(format!("remowt-agent-{arch}"))
+ }
+
+ fn local_binary(&self) -> Result<PathBuf> {
+ let arch = env::consts::ARCH;
+ let path = self.binary(arch);
+ ensure!(
+ path.is_file(),
+ "no local remowt-agent build for arch {arch} in bundle {}",
+ self.dir.display()
+ );
+ Ok(path)
+ }
+}
+
+async fn run(sess: &Handle<SshHandler>, cmd: &str) -> Result<(Option<u32>, Vec<u8>)> {
+ let ch = sess.channel_open_session().await?;
+ ch.exec(true, cmd).await?;
+
+ let mut child = SshExecChild::from_exec(ch);
+ drop(child.stdin);
+ drain_to_tracing(child.stderr, cmd.to_owned(), true);
+
+ let mut out = Vec::new();
+ child.stdout.read_to_end(&mut out).await?;
+ let code = child.exit.await.ok().flatten();
+ Ok((code, out))
+}
+
+async fn run_string_ok(sess: &Handle<SshHandler>, cmd: &str) -> Result<String> {
+ let (code, mut out) = run(sess, cmd).await?;
+ ensure!(
+ code == Some(0),
+ "remote command failed (exit {code:?}): {cmd}"
+ );
+ if !out.is_empty() {
+ ensure!(
+ out.ends_with(b"\n"),
+ "remote command was not newline-terminated: {cmd}: {out:?}"
+ );
+ out.pop();
+ }
+ String::from_utf8(out).context("expected utf8 output for command")
+}
+
+async fn deploy_agent(sess: &Handle<SshHandler>, bundle: &AgentBundle) -> Result<Utf8PathBuf> {
+ debug!("uname -a");
+ let arch = run_string_ok(sess, "uname -m").await?;
+ let hash = bundle
+ .hashes
+ .get(&arch)
+ .ok_or_else(|| anyhow!("no remowt-agent build for remote arch {arch:?}"))?;
+
+ debug!("get dir");
+ let cache = run_string_ok(sess, "echo \"$XDG_CACHE_HOME\"").await?;
+ let dir = if cache.is_empty() {
+ let home = run_string_ok(sess, "echo \"$HOME\"").await?;
+ ensure!(
+ !home.is_empty(),
+ "remote $HOME and $XDG_CACHE_HOME both empty"
+ );
+ Utf8PathBuf::from(home).join(".cache/remowt")
+ } else {
+ Utf8PathBuf::from(cache).join("remowt")
+ };
+ let path = dir.join(hash);
+
+ debug!("presence");
+ let (present, _) = run(sess, &format!("test -x {}", sh_quote(&path))).await?;
+ if present != Some(0) {
+ let bin = bundle.binary(&arch);
+ debug!("read");
+ let bytes = fs::read(&bin)
+ .await
+ .with_context(|| format!("reading agent binary {}", bin.display()))?;
+ debug!("upload");
+ upload_agent(sess, &dir, &path, bytes).await?;
+ }
+ Ok(path)
+}
+
+async fn upload_agent(
+ sess: &Handle<SshHandler>,
+ dir: &Utf8Path,
+ path: &Utf8Path,
+ bytes: Vec<u8>,
+) -> Result<()> {
+ debug!("mkdirp");
+ run_string_ok(sess, &format!("mkdir -p {}", sh_quote(dir))).await?;
+
+ let tmp = dir.join(format!("tmp.{}", Uuid::new_v4()));
+ let ch = sess.channel_open_session().await?;
+ debug!("cat");
+ ch.exec(true, format!("cat > {}", sh_quote(&tmp))).await?;
+
+ let mut child = SshExecChild::from_exec(ch);
+ child
+ .stdin
+ .write_all(&bytes)
+ .await
+ .context("sending agent binary")?;
+ child
+ .stdin
+ .shutdown()
+ .await
+ .context("sending agent binary")?;
+ let code = child.wait().await;
+ ensure!(code == Some(0), "agent upload failed (exit {code:?})");
+
+ debug!("chmod");
+ run_string_ok(sess, &format!("chmod 0755 {}", sh_quote(&tmp))).await?;
+ run_string_ok(
+ sess,
+ &format!("mv -f {} {}", sh_quote(&tmp), sh_quote(path)),
+ )
+ .await?;
+ Ok(())
+}
+
+pub struct SshHandler {
+ host: String,
+ port: u16,
+ subs: Subs,
+}
+impl Handler for SshHandler {
+ type Error = russh::Error;
+ async fn check_server_key(
+ &mut self,
+ server_public_key: &PublicKey,
+ ) -> Result<bool, Self::Error> {
+ Ok(check_known_hosts(&self.host, self.port, server_public_key)?)
+ }
+ async fn server_channel_open_forwarded_streamlocal(
+ &mut self,
+ channel: Channel<Msg>,
+ socket_path: &str,
+ _session: &mut Session,
+ ) -> Result<(), Self::Error> {
+ let Some(ch) = self
+ .subs
+ .lock()
+ .expect("lock")
+ .remove(&Utf8PathBuf::from(socket_path))
+ else {
+ return Err(russh::Error::WrongChannel);
+ };
+ let _ = ch.send(channel);
+ Ok(())
+ }
+}
+
+enum Transport {
+ Ssh {
+ sess: Arc<Handle<SshHandler>>,
+ subs: Subs,
+ runtime_dir: Utf8PathBuf,
+ agent_path: Utf8PathBuf,
+ },
+ Local {
+ agent_path: PathBuf,
+ runtime_dir: Utf8PathBuf,
+ },
+}
+
+struct RemowtInner {
+ transport: Transport,
+ rpc: Rpc<BifConfig>,
+ elevated: tokio::sync::OnceCell<()>,
+ #[allow(dead_code)]
+ children: Mutex<Vec<tokio::process::Child>>,
+ _runtime_tmp: Option<TempDir>,
+ user: String,
+ iroh: IrohState,
+}
+
+#[derive(Default)]
+struct IrohState {
+ conn: tokio::sync::OnceCell<iroh::endpoint::Connection>,
+ #[allow(dead_code)]
+ endpoint: Mutex<Option<iroh::Endpoint>>,
+ subs: Arc<Mutex<HashMap<u64, oneshot::Sender<RemowtStream>>>>,
+ next_token: AtomicU64,
+ router: Mutex<Option<Arc<DatagramRouter>>>,
+}
+
+#[derive(Clone)]
+pub struct Remowt(Arc<RemowtInner>);
+
+pub type RemowtRemote = Remote<BifConfig>;
+
+impl Remowt {
+ /// Connect to the remote host over ssh, detect the architecture and deploy the required
+ /// agent binary.
+ pub async fn connect(host: &str, bundle: &AgentBundle, remowt_user: String) -> Result<Self> {
+ let conf = russh_config::parse_home(host)?;
+ let port = conf.host_config.port.or(conf.port).unwrap_or(22);
+ let hostname = conf
+ .host_config
+ .hostname
+ .clone()
+ .unwrap_or_else(|| conf.host_name.clone());
+ let user = conf
+ .user
+ .clone()
+ .unwrap_or_else(|| env::var("USER").unwrap_or_else(|_| "root".to_owned()));
+
+ let subs: Subs = Arc::new(Mutex::new(HashMap::new()));
+ let mut sess = connect(
+ Arc::new(Config::default()),
+ (hostname.clone(), port),
+ SshHandler {
+ host: hostname,
+ port,
+ subs: subs.clone(),
+ },
+ )
+ .await?;
+
+ let mut agent = AgentClient::connect_env().await?;
+ let rsa_hash = sess.best_supported_rsa_hash().await?.flatten();
+ let mut authenticated = false;
+ for ident in agent.request_identities().await? {
+ let AgentIdentity::PublicKey { key, .. } = ident else {
+ continue;
+ };
+ if sess
+ .authenticate_publickey_with(user.clone(), key, rsa_hash, &mut agent)
+ .await?
+ .success()
+ {
+ authenticated = true;
+ break;
+ }
+ }
+ ensure!(authenticated, "ssh authentication failed");
+
+ let sess = Arc::new(sess);
+
+ debug!("deploying agent");
+ let agent_path = deploy_agent(&sess, bundle).await?;
+
+ debug!("runtime dir");
+ let runtime_dir = remote_runtime_dir(&sess).await?;
+
+ let rpc = Rpc::<BifConfig>::new(Address::User);
+
+ let cmd_chan = sess.channel_open_session().await?;
+ debug!("starting agent");
+ cmd_chan
+ .exec(true, format!("{} real-agent", sh_quote(&agent_path)))
+ .await?;
+
+ let child = SshExecChild::from_exec(cmd_chan);
+ drain_to_tracing(child.stderr, "agent".to_owned(), true);
+ rpc.add_direct(
+ Address::Agent,
+ child_port(child.stdout, child.stdin),
+ Rtt(0),
+ );
+
+ let remowt = Self(Arc::new(RemowtInner {
+ transport: Transport::Ssh {
+ sess,
+ subs,
+ runtime_dir,
+ agent_path,
+ },
+ rpc,
+ elevated: tokio::sync::OnceCell::new(),
+ children: Mutex::new(Vec::new()),
+ _runtime_tmp: None,
+ user: remowt_user,
+ iroh: IrohState::default(),
+ }));
+ remowt.setup_iroh().await;
+ Ok(remowt)
+ }
+
+ /// "Connect" to the local machine's agent, by starting the agent binary locally.
+ pub async fn connect_local(bundle: &AgentBundle, user: String) -> Result<Self> {
+ let agent_path = bundle.local_binary()?;
+ let mut child = tokio::process::Command::new(&agent_path)
+ .arg("real-agent")
+ .arg("--local")
+ .stdin(std::process::Stdio::piped())
+ .stdout(std::process::Stdio::piped())
+ .kill_on_drop(true)
+ .spawn()
+ .with_context(|| format!("spawning agent binary {}", agent_path.display()))?;
+ let stdin = child.stdin.take().expect("stdin piped");
+ let stdout = child.stdout.take().expect("stdout piped");
+
+ let rpc = Rpc::<BifConfig>::new(Address::User);
+ rpc.add_direct(Address::Agent, child_port(stdout, stdin), Rtt(0));
+
+ let (runtime_dir, runtime_tmp) = local_runtime_dir()?;
+
+ Ok(Self(Arc::new(RemowtInner {
+ transport: Transport::Local {
+ agent_path,
+ runtime_dir,
+ },
+ rpc,
+ elevated: tokio::sync::OnceCell::new(),
+ children: Mutex::new(vec![child]),
+ _runtime_tmp: runtime_tmp,
+ user,
+ iroh: IrohState::default(),
+ })))
+ }
+
+ /// Get the handle to the underlying russh session handle.
+ pub fn ssh(&self) -> Option<Arc<Handle<SshHandler>>> {
+ match &self.0.transport {
+ Transport::Ssh { sess, .. } => Some(sess.clone()),
+ Transport::Local { .. } => None,
+ }
+ }
+
+ pub fn rpc(&self) -> Rpc<BifConfig> {
+ self.0.rpc.clone()
+ }
+
+ pub async fn load_plugin(&self, id: u16, name: &str) -> Result<()> {
+ let client: PluginEndpointsClient<BifConfig> = self.endpoints();
+ client
+ .load_plugin(id, name.to_owned())
+ .await?
+ .map_err(|e| anyhow!("agent failed to load plugin: {e}"))
+ }
+ pub async fn run0_load_plugin_path(&self, id: u16, path: &str) -> Result<()> {
+ self.ensure_escalated().await?;
+ let client: PluginEndpointsClient<BifConfig> =
+ PluginEndpointsClient::wrap(self.0.rpc.remote(Address::AgentPrivileged));
+ client
+ .load_plugin_path(id, path.to_owned())
+ .await?
+ .map_err(|e| anyhow!("privileged agent failed to load plugin: {e}"))
+ }
+ pub fn plugin_endpoints<R: RemoteEndpoints<BifConfig>>(&self, id: u16) -> R {
+ R::wrap(self.0.rpc.remote(Address::Plugin(id)))
+ }
+
+ pub fn endpoints<R: RemoteEndpoints<BifConfig>>(&self) -> R {
+ R::wrap(self.0.rpc.remote(Address::Agent))
+ }
+ pub async fn run0_endpoints<R: RemoteEndpoints<BifConfig>>(&self) -> Result<R> {
+ self.ensure_escalated().await?;
+ Ok(R::wrap(self.0.rpc.remote(Address::AgentPrivileged)))
+ }
+
+ async fn ensure_escalated(&self) -> Result<()> {
+ self.0
+ .elevated
+ .get_or_try_init(|| async {
+ let (agent_path, local) = match &self.0.transport {
+ Transport::Ssh { agent_path, .. } => (agent_path.as_str().to_owned(), false),
+ Transport::Local { agent_path, .. } => (
+ agent_path
+ .to_str()
+ .ok_or_else(|| anyhow!("local agent path is not utf-8"))?
+ .to_owned(),
+ true,
+ ),
+ };
+
+ let (tool, flags) = self.detect_escalation().await?;
+ let mut args: Vec<String> = Vec::new();
+ args.push("-w".to_owned());
+ args.push(tool.to_owned());
+ args.extend(flags.iter().copied().map(str::to_owned));
+ if tool == "run0" {
+ args.push(format!(
+ "--unit={}-{}.service",
+ self.0.user,
+ Uuid::new_v4().simple()
+ ));
+ }
+ args.push(agent_path);
+ args.push("real-agent".to_owned());
+ args.push("--privileged".to_owned());
+ if local {
+ args.push("--local".to_owned());
+ }
+
+ let child = self
+ .spawn(SpawnOptions {
+ program: "setsid".to_owned(),
+ args,
+ stdin: StdioMode::Pipe,
+ stdout: StdioMode::Pipe,
+ stderr: StderrMode::Inherit,
+ ..Default::default()
+ })
+ .await
+ .context("spawning privileged agent")?;
+
+ let stdin = child
+ .stdin
+ .ok_or_else(|| anyhow!("privileged agent stdin missing"))?;
+ let stdout = child
+ .stdout
+ .ok_or_else(|| anyhow!("privileged agent stdout missing"))?;
+
+ let port = child_port(stdout, stdin);
+ self.0
+ .rpc
+ .add_direct(Address::AgentPrivileged, port, Rtt(0));
+ anyhow::Ok(())
+ })
+ .await?;
+ Ok(())
+ }
+
+ async fn detect_escalation(&self) -> Result<(&'static str, &'static [&'static str])> {
+ for (tool, flags) in ESCALATORS {
+ let probe = self
+ .spawn(SpawnOptions {
+ program: (*tool).to_owned(),
+ args: vec!["--version".to_owned()],
+ stdout: StdioMode::Null,
+ stderr: StderrMode::Null,
+ ..Default::default()
+ })
+ .await;
+ if let Ok(child) = probe {
+ let _ = child.wait().await;
+ return Ok((tool, flags));
+ }
+ }
+ bail!("no escalation tool found")
+ }
+
+ /// XDG_RUNTIME_DIR on the remote machine.
+ pub fn runtime_dir(&self) -> Utf8PathBuf {
+ match &self.0.transport {
+ Transport::Ssh { runtime_dir, .. } => runtime_dir.clone(),
+ Transport::Local { runtime_dir, .. } => runtime_dir.clone(),
+ }
+ }
+
+ /// Bind unix listener socket on the remote machine with auto-generated path, returning the path.
+ pub async fn bind_runtime_unix(&self, hint: &str) -> Result<(RemowtListener, Utf8PathBuf)> {
+ let sock = self
+ .runtime_dir()
+ .join(format!("remowt-{hint}-{}.sock", Uuid::new_v4()));
+ let listener = self.bind_unix(&sock).await?;
+ Ok((listener, sock))
+ }
+
+ /// Bind unix listener socket on the remote machine on the specified path.
+ pub async fn bind_unix(&self, path: &Utf8Path) -> Result<RemowtListener> {
+ match &self.0.transport {
+ Transport::Ssh { sess, subs, .. } => {
+ let (tx, rx) = oneshot::channel();
+ subs.lock().expect("lock").insert(path.to_owned(), tx);
+ sess.streamlocal_forward(path.to_owned()).await?;
+ Ok(RemowtListener::Ssh(rx))
+ }
+ Transport::Local { .. } => {
+ let _ = std::fs::remove_file(path);
+ Ok(RemowtListener::Local(
+ UnixListener::bind(path)?,
+ path.to_owned(),
+ ))
+ }
+ }
+ }
+
+ /// Bind a data tunnel, preferring the iroh fast path when it is up. Escalated tunnels
+ /// (the privileged agent, a separate process with no iroh connection of its own) and the
+ /// local transport always use the ssh/unix path.
+ pub async fn bind_fast_tunnel(
+ &self,
+ hint: &str,
+ escalated: bool,
+ ) -> Result<(RemowtListener, TunnelAddr)> {
+ if !escalated && self.0.iroh.conn.get().is_some() {
+ let token = self
+ .0
+ .iroh
+ .next_token
+ .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
+ let (tx, rx) = oneshot::channel();
+ self.0.iroh.subs.lock().expect("lock").insert(token, tx);
+ return Ok((RemowtListener::Iroh(rx), TunnelAddr::Iroh { token }));
+ }
+ let (listener, path) = self.bind_runtime_unix(hint).await?;
+ Ok((listener, TunnelAddr::Unix(path)))
+ }
+
+ async fn setup_iroh(&self) {
+ if std::env::var_os("REMOWT_NO_IROH").is_some() {
+ debug!("REMOWT_NO_IROH set, skipping iroh fast tunnel");
+ return;
+ }
+ if !matches!(self.0.transport, Transport::Ssh { .. }) {
+ return;
+ }
+ if let Err(e) = self.try_setup_iroh().await {
+ warn!("iroh fast tunnel unavailable, using ssh: {e}");
+ }
+ }
+
+ async fn try_setup_iroh(&self) -> Result<()> {
+ use remowt_endpoints::iroh_tunnel::IrohTunnelClient;
+ use remowt_link_shared::iroh_tunnel::{build_endpoint, ssh_custom_addr, REMOWT_ALPN};
+
+ let (listener, sock) = self.bind_runtime_unix("iroh-xport").await?;
+ let secret = iroh::SecretKey::generate();
+ let client_id = secret.public();
+
+ let client: IrohTunnelClient<BifConfig> =
+ IrohTunnelClient::wrap(self.0.rpc.remote(Address::Agent));
+ let (accepted, agent_id) = tokio::join!(listener.accept(), client.setup(client_id, sock));
+ let stream = accepted?;
+ let agent_id = agent_id?.map_err(|e| anyhow!("agent iroh setup failed: {e}"))?;
+
+ let ep = build_endpoint(secret, stream, agent_id, false).await?;
+ let addr = iroh::EndpointAddr::from_parts(
+ agent_id,
+ [iroh::TransportAddr::Custom(ssh_custom_addr(agent_id))],
+ );
+ let conn = ep.connect(addr, REMOWT_ALPN).await?;
+ ensure!(conn.remote_id() == agent_id, "iroh peer identity mismatch");
+ info!("iroh fast tunnel established");
+
+ let subs = self.0.iroh.subs.clone();
+ let accept_conn = conn.clone();
+ tokio::spawn(async move {
+ loop {
+ match accept_conn.accept_bi().await {
+ Ok((send, mut recv)) => {
+ let subs = subs.clone();
+ tokio::spawn(async move {
+ let mut buf = [0u8; 8];
+ if tokio::io::AsyncReadExt::read_exact(&mut recv, &mut buf)
+ .await
+ .is_err()
+ {
+ return;
+ }
+ let token = u64::from_be_bytes(buf);
+ let tx = subs.lock().expect("lock").remove(&token);
+ if let Some(tx) = tx {
+ let _ = tx.send(RemowtStream::Iroh(IrohBiStream::new(send, recv)));
+ }
+ });
+ }
+ Err(e) => {
+ debug!("iroh accept loop ended: {e}");
+ break;
+ }
+ }
+ }
+ });
+
+ let log_conn = conn.clone();
+ tokio::spawn(async move {
+ tokio::time::sleep(std::time::Duration::from_secs(2)).await;
+ for p in log_conn.paths().iter() {
+ if p.is_selected() {
+ info!(rtt = ?p.rtt(), "iroh selected path: {}", p.remote_addr());
+ }
+ }
+ });
+
+ *self.0.iroh.router.lock().expect("lock") = Some(DatagramRouter::spawn(conn.clone()));
+ *self.0.iroh.endpoint.lock().expect("lock") = Some(ep);
+ let _ = self.0.iroh.conn.set(conn);
+ Ok(())
+ }
+
+ pub fn datagram_router(&self) -> Option<Arc<DatagramRouter>> {
+ self.0.iroh.router.lock().expect("lock").clone()
+ }
+}
+
+pub(crate) fn drain_to_tracing(
+ stream: impl AsyncRead + Unpin + 'static + Send,
+ context: String,
+ stderr: bool,
+) -> JoinHandle<()> {
+ tokio::spawn(async move {
+ let mut reader = BufReader::new(stream);
+ let mut buf = Vec::with_capacity(4096);
+ loop {
+ buf.clear();
+ match reader.read_until(b'\n', &mut buf).await {
+ Ok(0) => break,
+ Ok(_) => {
+ let line = String::from_utf8_lossy(buf.strip_suffix(b"\n").unwrap_or(&buf));
+ if stderr {
+ warn!(context = %context, "{line}");
+ } else {
+ info!(context = %context, "{line}");
+ }
+ }
+ Err(e) => {
+ warn!(context = %context, "child stdio read failed: {e}");
+ break;
+ }
+ }
+ }
+ })
+}
+
+fn local_runtime_dir() -> Result<(Utf8PathBuf, Option<TempDir>)> {
+ if let Ok(dir) = env::var("XDG_RUNTIME_DIR") {
+ if !dir.is_empty() {
+ return Ok((Utf8PathBuf::from(dir), None));
+ }
+ }
+ let tmp = tempfile::Builder::new()
+ .prefix("remowt.")
+ .rand_bytes(12)
+ .tempdir()?;
+ let dir = Utf8PathBuf::from_path_buf(tmp.path().to_owned())
+ .map_err(|p| anyhow!("temp dir {} is not utf-8", p.display()))?;
+ Ok((dir, Some(tmp)))
+}
+
+async fn remote_runtime_dir(sess: &Handle<SshHandler>) -> Result<Utf8PathBuf> {
+ let dir = run_string_ok(sess, "echo \"$XDG_RUNTIME_DIR\"").await?;
+ let dir = dir.trim();
+ if dir.is_empty() {
+ let tmp = run_string_ok(sess, "mktemp -d remowt.XXXXXXXXXXXX --tmpdir").await?;
+ Ok(Utf8PathBuf::from(tmp))
+ } else {
+ Ok(Utf8PathBuf::from(dir))
+ }
+}
remowt/crates/remowt-client/src/shell.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-client/src/shell.rs
@@ -0,0 +1,65 @@
+use anyhow::{anyhow, Result};
+use bifrostlink::declarative::RemoteEndpoints as _;
+use bifrostlink::Remote;
+use remowt_endpoints::pty::{PtyClient, ShellId};
+use remowt_link_shared::{Address, BifConfig};
+
+use crate::forwarded::RemowtStream;
+use crate::Remowt;
+
+pub struct RemowtShell {
+ pub id: ShellId,
+ pub stream: RemowtStream,
+ remote: Remote<BifConfig>,
+}
+impl RemowtShell {
+ pub fn resizer(&self) -> RemowtShellResizer {
+ RemowtShellResizer {
+ remote: self.remote.clone(),
+ id: self.id,
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct RemowtShellResizer {
+ remote: Remote<BifConfig>,
+ id: ShellId,
+}
+
+impl RemowtShellResizer {
+ pub async fn resize(&self, cols: u16, rows: u16) -> Result<()> {
+ PtyClient::wrap(self.remote.clone())
+ .resize(self.id, cols, rows)
+ .await?
+ .map_err(|e| anyhow!("failed to resize remote shell: {e}"))
+ }
+}
+
+impl Remowt {
+ pub async fn open_shell(
+ &self,
+ term: &str,
+ cols: u16,
+ rows: u16,
+ escalate: bool,
+ ) -> Result<RemowtShell> {
+ let (forwarded, tunnel) = self.bind_fast_tunnel("shell", escalate).await?;
+ let client: PtyClient<BifConfig> = if escalate {
+ self.run0_endpoints().await?
+ } else {
+ self.endpoints()
+ };
+ let id = client
+ .open_shell(tunnel, term.to_owned(), cols, rows)
+ .await?
+ .map_err(|e| anyhow!("agent failed to open shell: {e}"))?;
+ let stream = forwarded.accept().await?;
+
+ Ok(RemowtShell {
+ id,
+ stream,
+ remote: self.0.rpc.remote(Address::Agent),
+ })
+ }
+}
remowt/crates/remowt-client/src/ssh_exec.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-client/src/ssh_exec.rs
@@ -0,0 +1,82 @@
+use bytes::Bytes;
+use russh::client::Msg;
+use russh::{Channel, ChannelMsg};
+use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _, DuplexStream};
+use tokio::sync::oneshot;
+
+const BUF: usize = 64 * 1024;
+
+pub(crate) struct SshExecChild {
+ pub stdin: DuplexStream,
+ pub stdout: DuplexStream,
+ pub stderr: DuplexStream,
+ pub exit: oneshot::Receiver<Option<u32>>,
+}
+
+impl SshExecChild {
+ /// Manage channel returned by russh exec().
+ pub(crate) fn from_exec(ch: Channel<Msg>) -> Self {
+ let (stdin, mut stdin_r) = tokio::io::duplex(BUF);
+ let (mut out_w, stdout) = tokio::io::duplex(BUF);
+ let (mut err_w, stderr) = tokio::io::duplex(BUF);
+ let (exit_tx, exit) = oneshot::channel();
+
+ tokio::spawn(async move {
+ let (mut read, write) = ch.split();
+
+ let stdin_pump = tokio::spawn(async move {
+ let mut buf = vec![0u8; BUF];
+ loop {
+ match stdin_r.read(&mut buf).await {
+ Ok(0) | Err(_) => break,
+ Ok(n) => {
+ if write
+ .data_bytes(Bytes::copy_from_slice(&buf[..n]))
+ .await
+ .is_err()
+ {
+ return;
+ }
+ }
+ }
+ }
+ let _ = write.eof().await;
+ });
+
+ let mut code = None;
+ while let Some(msg) = read.wait().await {
+ match msg {
+ ChannelMsg::Data { data } => {
+ if out_w.write_all(&data).await.is_err() {
+ break;
+ }
+ }
+ ChannelMsg::ExtendedData { data, .. } => {
+ if err_w.write_all(&data).await.is_err() {
+ break;
+ }
+ }
+ ChannelMsg::ExitStatus { exit_status } => code = Some(exit_status),
+ _ => {}
+ }
+ }
+
+ stdin_pump.abort();
+ let _ = out_w.shutdown().await;
+ let _ = err_w.shutdown().await;
+ let _ = exit_tx.send(code);
+ });
+
+ SshExecChild {
+ stdin,
+ stdout,
+ stderr,
+ exit,
+ }
+ }
+
+ /// Wait for the process to finish, returning its exit status.
+ pub(crate) async fn wait(self) -> Option<u32> {
+ self.exit.await.ok().flatten()
+ }
+}
remowt/crates/remowt-client/src/subprocess.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-client/src/subprocess.rs
@@ -0,0 +1,439 @@
+use std::pin::pin;
+
+use anyhow::{anyhow, bail, Result};
+use camino::Utf8PathBuf;
+use futures::StreamExt as _;
+use remowt_endpoints::subprocess::{ProcId, SpawnSpec, StderrSpec, StdioSpec, SubprocessClient};
+use remowt_link_shared::iroh_tunnel::TunnelAddr;
+use remowt_link_shared::BifConfig;
+use serde::de::DeserializeOwned;
+use tokio::io::AsyncWriteExt as _;
+use tokio::select;
+use tokio_util::codec::{BytesCodec, FramedRead, LinesCodec};
+use tracing::{debug, info, warn};
+
+use crate::forwarded::{RemowtListener, RemowtStream};
+use crate::{drain_to_tracing, Remowt};
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub enum StdioMode {
+ #[default]
+ Null,
+ Pipe,
+ Inherit,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub enum StderrMode {
+ #[default]
+ Null,
+ Pipe,
+ Inherit,
+ MergeWithStdout,
+}
+
+#[derive(Default)]
+pub struct SpawnOptions {
+ pub program: String,
+ pub args: Vec<String>,
+ pub env: Vec<(String, String)>,
+ pub env_clear: bool,
+ pub cwd: Option<Utf8PathBuf>,
+ pub escalated: bool,
+ pub stdin: StdioMode,
+ pub stdout: StdioMode,
+ pub stderr: StderrMode,
+}
+
+pub struct RemowtChild {
+ pub stdin: Option<RemowtStream>,
+ pub stdout: Option<RemowtStream>,
+ pub stderr: Option<RemowtStream>,
+ id: ProcId,
+ client: SubprocessClient<BifConfig>,
+}
+
+impl RemowtChild {
+ pub async fn wait(self) -> Result<Option<i32>> {
+ let RemowtChild {
+ stdin,
+ stdout,
+ stderr,
+ id,
+ client,
+ } = self;
+ drop(stdin);
+ let drain_out = async move {
+ if let Some(s) = stdout {
+ let _ = drain_to_tracing(s, "<child stdout>".to_owned(), false).await;
+ }
+ };
+ let drain_err = async move {
+ if let Some(s) = stderr {
+ let _ = drain_to_tracing(s, "<child stderr>".to_owned(), true).await;
+ }
+ };
+ let wait = async move {
+ client
+ .wait(id)
+ .await?
+ .map_err(|e| anyhow!("agent wait failed: {e}"))
+ };
+ let (code, _, _) = tokio::join!(wait, drain_out, drain_err);
+ code
+ }
+
+ pub async fn kill(&self, signal: i32) -> Result<()> {
+ self.client
+ .kill(self.id, signal)
+ .await?
+ .map_err(|e| anyhow!("agent kill failed: {e}"))
+ }
+}
+
+fn needs_socket(m: StdioMode) -> bool {
+ matches!(m, StdioMode::Pipe | StdioMode::Inherit)
+}
+
+fn stderr_needs_socket(m: StderrMode) -> bool {
+ matches!(m, StderrMode::Pipe | StderrMode::Inherit)
+}
+
+impl Remowt {
+ pub async fn spawn(&self, opts: SpawnOptions) -> Result<RemowtChild> {
+ let SpawnOptions {
+ program,
+ args,
+ env,
+ env_clear,
+ cwd,
+ escalated,
+ stdin,
+ stdout,
+ stderr,
+ } = opts;
+
+ if matches!(stderr, StderrMode::MergeWithStdout) && !needs_socket(stdout) {
+ bail!("stderr=MergeWithStdout requires stdout=Pipe or Inherit");
+ }
+
+ let stdin_bound = if needs_socket(stdin) {
+ Some(self.bind_fast_tunnel("proc-stdin", escalated).await?)
+ } else {
+ None
+ };
+ let stdout_bound = if needs_socket(stdout) {
+ Some(self.bind_fast_tunnel("proc-stdout", escalated).await?)
+ } else {
+ None
+ };
+ let stderr_bound = if stderr_needs_socket(stderr) {
+ Some(self.bind_fast_tunnel("proc-stderr", escalated).await?)
+ } else {
+ None
+ };
+
+ let stdin_spec = match &stdin_bound {
+ Some((_, t)) => StdioSpec::Tunnel(t.clone()),
+ None => StdioSpec::Null,
+ };
+ let stdout_spec = match &stdout_bound {
+ Some((_, t)) => StdioSpec::Tunnel(t.clone()),
+ None => StdioSpec::Null,
+ };
+ let stderr_spec = match (&stderr, &stderr_bound) {
+ (StderrMode::Pipe | StderrMode::Inherit, Some((_, t))) => StderrSpec::Tunnel(t.clone()),
+ (StderrMode::MergeWithStdout, _) => StderrSpec::MergeWithStdout,
+ _ => StderrSpec::Null,
+ };
+
+ let client: SubprocessClient<BifConfig> = if escalated {
+ // Boxed to break the async-fn type cycle
+ Box::pin(self.run0_endpoints::<SubprocessClient<BifConfig>>()).await?
+ } else {
+ self.endpoints()
+ };
+
+ let spec = SpawnSpec {
+ program: program.clone(),
+ args,
+ env,
+ env_clear,
+ cwd,
+ stdin: stdin_spec,
+ stdout: stdout_spec,
+ stderr: stderr_spec,
+ };
+ let id = client
+ .spawn(spec)
+ .await?
+ .map_err(|e| anyhow!("agent spawn failed: {e}"))?;
+
+ let (stdin_res, stdout_res, stderr_res) = tokio::join!(
+ accept(stdin_bound),
+ accept(stdout_bound),
+ accept(stderr_bound),
+ );
+
+ let stdin_stream = handle_stdin(stdin, stdin_res?, &program);
+ let stdout_stream = handle_output(stdout, stdout_res?, &program);
+ let stderr_stream = handle_output_err(stderr, stderr_res?, &program);
+
+ Ok(RemowtChild {
+ stdin: stdin_stream,
+ stdout: stdout_stream,
+ stderr: stderr_stream,
+ id,
+ client,
+ })
+ }
+
+ pub fn cmd(&self, program: impl AsRef<str>) -> RemowtCommand {
+ let program = program.as_ref().to_owned();
+ RemowtCommand {
+ program,
+ args: vec![],
+ env: vec![],
+ remowt: self.clone(),
+ escalated: false,
+ }
+ }
+}
+
+async fn accept(b: Option<(RemowtListener, TunnelAddr)>) -> Result<Option<RemowtStream>> {
+ match b {
+ Some((l, _)) => Ok(Some(l.accept().await?)),
+ None => Ok(None),
+ }
+}
+
+fn handle_stdin(mode: StdioMode, s: Option<RemowtStream>, program: &str) -> Option<RemowtStream> {
+ match mode {
+ StdioMode::Pipe => s,
+ StdioMode::Inherit => {
+ if let Some(s) = s {
+ let program = program.to_owned();
+ tokio::spawn(async move {
+ let mut stdin = tokio::io::stdin();
+ let mut s = s;
+ if let Err(e) = tokio::io::copy(&mut stdin, &mut s).await {
+ warn!(program, "stdin forward ended: {e}");
+ }
+ let _ = s.shutdown().await;
+ });
+ }
+ None
+ }
+ StdioMode::Null => None,
+ }
+}
+
+fn handle_output(mode: StdioMode, s: Option<RemowtStream>, program: &str) -> Option<RemowtStream> {
+ match mode {
+ StdioMode::Pipe => s,
+ StdioMode::Inherit => {
+ if let Some(s) = s {
+ let program = program.to_owned();
+ tokio::spawn(drain_to_tracing(s, program, false));
+ }
+ None
+ }
+ StdioMode::Null => None,
+ }
+}
+
+fn handle_output_err(
+ mode: StderrMode,
+ s: Option<RemowtStream>,
+ program: &str,
+) -> Option<RemowtStream> {
+ match mode {
+ StderrMode::Pipe => s,
+ StderrMode::Inherit => {
+ if let Some(s) = s {
+ let program = program.to_owned();
+ tokio::spawn(drain_to_tracing(s, program, true));
+ }
+ None
+ }
+ StderrMode::MergeWithStdout | StderrMode::Null => None,
+ }
+}
+
+fn escape_bash(input: &str, out: &mut String) {
+ const TO_ESCAPE: &str = "$ !\"#&'()*,;<>?[\\]^`{|}";
+ if input.chars().all(|c| !TO_ESCAPE.contains(c)) {
+ out.push_str(input);
+ return;
+ }
+ out.push('\'');
+ for (i, v) in input.split('\'').enumerate() {
+ if i != 0 {
+ out.push_str("'\"'\"'");
+ }
+ out.push_str(v);
+ }
+ out.push('\'');
+}
+
+#[derive(Clone)]
+pub struct RemowtCommand {
+ program: String,
+ args: Vec<String>,
+ env: Vec<(String, String)>,
+ remowt: Remowt,
+ escalated: bool,
+}
+
+impl RemowtCommand {
+ pub fn arg(&mut self, arg: impl AsRef<str>) -> &mut Self {
+ self.args.push(arg.as_ref().to_owned());
+ self
+ }
+ pub fn args<V: AsRef<str>>(&mut self, args: impl IntoIterator<Item = V>) -> &mut Self {
+ for arg in args {
+ self.args.push(arg.as_ref().to_owned());
+ }
+ self
+ }
+ pub fn eqarg(&mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> &mut Self {
+ self.args
+ .push(format!("{}={}", key.as_ref(), value.as_ref()));
+ self
+ }
+ pub fn comparg(&mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> &mut Self {
+ self.args.push(key.as_ref().to_owned());
+ self.args.push(value.as_ref().to_owned());
+ self
+ }
+ pub fn env(&mut self, name: impl AsRef<str>, value: impl AsRef<str>) -> &mut Self {
+ self.env
+ .push((name.as_ref().to_owned(), value.as_ref().to_owned()));
+ self
+ }
+
+ pub fn sudo(mut self) -> Self {
+ self.escalated = true;
+ self
+ }
+
+ /// Only for display.
+ fn shell_line(&self) -> String {
+ let mut out = String::new();
+ if self.escalated {
+ out.push_str("run0 ");
+ }
+ if !self.env.is_empty() {
+ out.push_str("env");
+ for (k, v) in &self.env {
+ out.push(' ');
+ assert!(!k.contains('='));
+ escape_bash(k, &mut out);
+ out.push('=');
+ escape_bash(v, &mut out);
+ }
+ out.push(' ');
+ }
+ escape_bash(&self.program, &mut out);
+ for arg in &self.args {
+ out.push(' ');
+ escape_bash(arg, &mut out);
+ }
+ out
+ }
+
+ fn into_spawn_options(self) -> (Remowt, SpawnOptions, String) {
+ let line = self.shell_line();
+ let opts = SpawnOptions {
+ program: self.program,
+ args: self.args,
+ env: self.env,
+ env_clear: false,
+ cwd: None,
+ escalated: self.escalated,
+ stdin: StdioMode::Null,
+ stdout: StdioMode::Pipe,
+ stderr: StderrMode::Pipe,
+ };
+ (self.remowt, opts, line)
+ }
+
+ pub async fn run(self) -> Result<()> {
+ run_inner(self, false).await.map(|_| ())
+ }
+ pub async fn run_string(self) -> Result<String> {
+ let bytes = run_inner(self, true).await?.expect("want_stdout");
+ Ok(String::from_utf8(bytes)?)
+ }
+ pub async fn run_value<T: DeserializeOwned>(self) -> Result<T> {
+ let s = self.run_string().await?;
+ Ok(serde_json::from_str(&s)?)
+ }
+}
+
+async fn run_inner(cmd: RemowtCommand, want_stdout: bool) -> Result<Option<Vec<u8>>> {
+ let (remowt, opts, line) = cmd.into_spawn_options();
+ debug!("running command {line:?} over remowt");
+ let program = opts.program.clone();
+ let mut child = remowt.spawn(opts).await?;
+ let stderr = child.stderr.take().expect("stderr=Pipe");
+ let stdout = child.stdout.take().expect("stdout=Pipe");
+
+ let mut err = FramedRead::new(stderr, LinesCodec::new());
+ let (mut out_bytes, mut out_lines) = if want_stdout {
+ (Some(FramedRead::new(stdout, BytesCodec::new())), None)
+ } else {
+ (None, Some(FramedRead::new(stdout, LinesCodec::new())))
+ };
+
+ let mut buf = if want_stdout { Some(Vec::new()) } else { None };
+
+ let mut wait = pin!(child.wait());
+ let exit = loop {
+ select! {
+ biased;
+
+ Some(e) = err.next() => {
+ let e = e?;
+ warn!(program = %program, "{e}");
+ }
+ Some(o) = async { out_bytes.as_mut()?.next().await }, if want_stdout => {
+ buf.as_mut().expect("want_stdout").extend_from_slice(&o?);
+ }
+ Some(o) = async { out_lines.as_mut()?.next().await }, if !want_stdout => {
+ let o = o?;
+ info!(program = %program, "{o}");
+ }
+ res = &mut wait => {
+ break res?;
+ }
+ }
+ };
+
+ while let Some(e) = err.next().await {
+ if let Ok(line) = e {
+ warn!(program = %program, "{line}");
+ }
+ }
+ if want_stdout {
+ if let Some(out_bytes) = out_bytes.as_mut() {
+ while let Some(o) = out_bytes.next().await {
+ if let Ok(chunk) = o {
+ buf.as_mut().expect("want_stdout").extend_from_slice(&chunk);
+ }
+ }
+ }
+ } else if let Some(out_lines) = out_lines.as_mut() {
+ while let Some(o) = out_lines.next().await {
+ if let Ok(line) = o {
+ info!(program = %program, "{line}");
+ }
+ }
+ }
+
+ match exit {
+ Some(0) => Ok(buf),
+ Some(c) => bail!("command '{line}' failed with status {c}"),
+ None => Err(anyhow!("command '{line}' ended without an exit status")),
+ }
+}
remowt/crates/remowt-endpoints/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-endpoints/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "remowt-endpoints"
+description = "Nix daemon proxy"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+bifrostlink.workspace = true
+camino.workspace = true
+serde = { workspace = true }
+tempfile.workspace = true
+thiserror.workspace = true
+tokio = { workspace = true, features = [
+ "net",
+ "io-util",
+ "rt",
+ "process",
+ "io-std",
+] }
+tracing.workspace = true
+nix = { workspace = true, features = ["process", "signal", "term"] }
+zbus.workspace = true
+remowt-link-shared.workspace = true
+iroh.workspace = true
remowt/crates/remowt-endpoints/src/forward.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-endpoints/src/forward.rs
@@ -0,0 +1,117 @@
+use std::collections::HashMap;
+use std::net::SocketAddr;
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::{Arc, Mutex};
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use remowt_link_shared::iroh_tunnel::{TunnelAddr, TunnelDialer};
+use serde::{Deserialize, Serialize};
+use std::result::Result;
+use tokio::net::{TcpStream, UdpSocket};
+use tracing::warn;
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("tunnel unavailable: {0}")]
+ Tunnel(String),
+ #[error("connect to {0} failed: {1}")]
+ Connect(String, String),
+ #[error("invalid target address {0:?}")]
+ BadAddr(String),
+ #[error("udp forward requires the iroh fast path, which is not established")]
+ NoIroh,
+}
+
+#[derive(Clone)]
+pub struct Forward {
+ dialer: Arc<TunnelDialer>,
+ next_session: Arc<AtomicU64>,
+}
+
+impl Forward {
+ pub fn new(dialer: Arc<TunnelDialer>) -> Self {
+ Self {
+ dialer,
+ next_session: Arc::new(AtomicU64::new(0)),
+ }
+ }
+}
+
+#[endpoints(ns = 12)]
+impl Forward {
+ #[endpoints(id = 1)]
+ async fn connect_tcp(&self, tunnel: TunnelAddr, addr: String) -> Result<(), Error> {
+ let stream = self
+ .dialer
+ .connect_tunnel(&tunnel)
+ .await
+ .map_err(|e| Error::Tunnel(e.to_string()))?;
+ let tcp = TcpStream::connect(&addr)
+ .await
+ .map_err(|e| Error::Connect(addr, e.to_string()))?;
+ tokio::spawn(async move {
+ let mut stream = stream;
+ let mut tcp = tcp;
+ let _ = tokio::io::copy_bidirectional(&mut stream, &mut tcp).await;
+ });
+ Ok(())
+ }
+
+ #[endpoints(id = 2)]
+ async fn open_udp(&self, addr: String) -> Result<u64, Error> {
+ let target: SocketAddr = addr.parse().map_err(|_| Error::BadAddr(addr.clone()))?;
+ let router = self.dialer.router().ok_or(Error::NoIroh)?;
+ let session = self.next_session.fetch_add(1, Ordering::Relaxed);
+ let mut rx = router.register(session);
+
+ let sockets: Arc<Mutex<HashMap<u64, Arc<UdpSocket>>>> =
+ Arc::new(Mutex::new(HashMap::new()));
+ tokio::spawn(async move {
+ while let Some((sub, payload)) = rx.recv().await {
+ let existing = sockets.lock().expect("lock").get(&sub).cloned();
+ let sock = match existing {
+ Some(s) => s,
+ None => {
+ let sock = match UdpSocket::bind(unspecified_for(&target)).await {
+ Ok(s) => s,
+ Err(e) => {
+ warn!("udp forward: bind failed: {e}");
+ continue;
+ }
+ };
+ if let Err(e) = sock.connect(target).await {
+ warn!("udp forward: connect {target} failed: {e}");
+ continue;
+ }
+ let sock = Arc::new(sock);
+ sockets.lock().expect("lock").insert(sub, sock.clone());
+ // Reply reader: datagrams from the target go back on (session, sub).
+ let router = router.clone();
+ let reply_sock = sock.clone();
+ tokio::spawn(async move {
+ let mut buf = vec![0u8; 65535];
+ while let Ok(n) = reply_sock.recv(&mut buf).await {
+ if router.send(session, sub, &buf[..n]).is_err() {
+ break;
+ }
+ }
+ });
+ sock
+ }
+ };
+ let _ = sock.send(&payload).await;
+ }
+ router.unregister(session);
+ });
+
+ Ok(session)
+ }
+}
+
+fn unspecified_for(target: &SocketAddr) -> SocketAddr {
+ match target {
+ SocketAddr::V4(_) => SocketAddr::from(([0, 0, 0, 0], 0)),
+ SocketAddr::V6(_) => SocketAddr::from(([0u16; 8], 0)),
+ }
+}
remowt/crates/remowt-endpoints/src/fs.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-endpoints/src/fs.rs
@@ -0,0 +1,105 @@
+use std::io::ErrorKind;
+use std::str::FromStr;
+use std::sync::Mutex;
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use camino::Utf8PathBuf;
+use serde::{Deserialize, Serialize};
+use tempfile::TempDir;
+
+#[derive(Default)]
+pub struct Fs {
+ tempdirs: Mutex<Vec<TempDir>>,
+}
+
+impl Fs {
+ pub fn new() -> Self {
+ Self::default()
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("file not found")]
+ NotFound,
+ #[error("file name/contents is not utf8")]
+ InvalidUtf8,
+ #[error("unknown fs error")]
+ Unknown,
+}
+
+#[endpoints(ns = 1)]
+impl Fs {
+ #[endpoints(id = 1)]
+ async fn read_file_tiny(&self, path: Utf8PathBuf) -> Result<Vec<u8>, Error> {
+ match tokio::fs::read(path).await {
+ Ok(v) => Ok(v),
+ Err(e) if e.kind() == ErrorKind::NotFound => Err(Error::NotFound),
+ _ => Err(Error::Unknown),
+ }
+ }
+ #[endpoints(id = 2)]
+ async fn file_exists(&self, path: Utf8PathBuf) -> bool {
+ tokio::fs::try_exists(path).await.unwrap_or(false)
+ }
+ #[endpoints(id = 3)]
+ async fn read_dir_raw(&self, path: Utf8PathBuf) -> Result<Vec<Utf8PathBuf>, Error> {
+ let mut dir = match tokio::fs::read_dir(path).await {
+ Ok(dir) => dir,
+ Err(e) if e.kind() == ErrorKind::NotFound => return Err(Error::NotFound),
+ Err(_) => return Err(Error::Unknown),
+ };
+ let mut out = Vec::new();
+ while let Ok(Some(entry)) = dir.next_entry().await {
+ let name = Utf8PathBuf::try_from(entry.file_name()).map_err(|_| Error::InvalidUtf8)?;
+ out.push(name);
+ }
+ Ok(out)
+ }
+ #[endpoints(id = 4)]
+ async fn mktemp_dir_raw(&self) -> Result<Utf8PathBuf, Error> {
+ let dir = tempfile::Builder::new()
+ .prefix("remowt.")
+ .tempdir()
+ .map_err(|_| Error::Unknown)?;
+ let mut tempdirs = self.tempdirs.lock().expect("not poisoned");
+ let path = Utf8PathBuf::try_from(dir.path().to_owned()).map_err(|_| Error::InvalidUtf8);
+ tempdirs.push(dir);
+ path
+ }
+ #[endpoints(id = 5)]
+ async fn rm_file(&self, path: Utf8PathBuf) -> Result<(), Error> {
+ match tokio::fs::remove_file(path).await {
+ Ok(()) => Ok(()),
+ Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
+ Err(_) => Err(Error::Unknown),
+ }
+ }
+}
+
+impl<C: Config> FsClient<C> {
+ pub async fn read_file_text(&self, path: impl Into<Utf8PathBuf>) -> Result<String, Error> {
+ let v = self
+ .read_file_tiny(path.into())
+ .await
+ .map_err(|_| Error::Unknown)?;
+ let v = v?;
+ String::from_utf8(v).map_err(|_| Error::InvalidUtf8)
+ }
+ pub async fn read_file_value<T: FromStr>(
+ &self,
+ path: impl Into<Utf8PathBuf>,
+ ) -> Result<Result<T, T::Err>, Error> {
+ let text = self.read_file_text(path).await?;
+ Ok(T::from_str(&text))
+ }
+ pub async fn mktemp_dir(&self) -> Result<Utf8PathBuf, Error> {
+ self.mktemp_dir_raw().await.map_err(|_| Error::Unknown)?
+ }
+ pub async fn read_dir(&self, path: impl Into<Utf8PathBuf>) -> Result<Vec<Utf8PathBuf>, Error> {
+ self.read_dir_raw(path.into())
+ .await
+ .map_err(|_| Error::Unknown)?
+ }
+}
remowt/crates/remowt-endpoints/src/iroh_tunnel.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-endpoints/src/iroh_tunnel.rs
@@ -0,0 +1,85 @@
+use std::sync::{Arc, Mutex};
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use camino::Utf8PathBuf;
+use iroh::{Endpoint, EndpointId, SecretKey};
+use remowt_link_shared::iroh_tunnel::{build_endpoint, TunnelDialer};
+use serde::{Deserialize, Serialize};
+use std::result::Result;
+use tokio::net::UnixStream;
+use tracing::{debug, warn};
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("iroh transport pipe unavailable: {0}")]
+ Pipe(String),
+ #[error("iroh endpoint failed: {0}")]
+ Iroh(String),
+}
+
+#[derive(Clone)]
+pub struct IrohTunnel {
+ dialer: Arc<TunnelDialer>,
+ endpoint: Arc<Mutex<Option<Endpoint>>>,
+}
+
+impl IrohTunnel {
+ pub fn new(dialer: Arc<TunnelDialer>) -> Self {
+ Self {
+ dialer,
+ endpoint: Arc::new(Mutex::new(None)),
+ }
+ }
+}
+
+#[endpoints(ns = 11)]
+impl IrohTunnel {
+ #[endpoints(id = 1)]
+ async fn setup(
+ &self,
+ client_id: EndpointId,
+ xport_socket: Utf8PathBuf,
+ ) -> Result<EndpointId, Error> {
+ let stream = UnixStream::connect(&xport_socket)
+ .await
+ .map_err(|e| Error::Pipe(e.to_string()))?;
+
+ let secret = SecretKey::generate();
+ let agent_id = secret.public();
+ let ep = build_endpoint(secret, stream, client_id, true)
+ .await
+ .map_err(|e| Error::Iroh(e.to_string()))?;
+
+ let dialer = self.dialer.clone();
+ let accept_ep = ep.clone();
+ tokio::spawn(async move {
+ while let Some(incoming) = accept_ep.accept().await {
+ let dialer = dialer.clone();
+ match incoming.accept() {
+ Ok(accepting) => {
+ tokio::spawn(async move {
+ match accepting.await {
+ Ok(conn) => {
+ if conn.remote_id() != client_id {
+ warn!("iroh: rejecting connection from unexpected peer");
+ conn.close(0u32.into(), b"unexpected peer");
+ return;
+ }
+ debug!("iroh tunnel connection accepted");
+ dialer.set_conn(conn);
+ }
+ Err(e) => warn!("iroh accept failed: {e}"),
+ }
+ });
+ }
+ Err(e) => warn!("iroh incoming rejected: {e}"),
+ }
+ }
+ });
+
+ *self.endpoint.lock().expect("not poisoned") = Some(ep);
+
+ Ok(agent_id)
+ }
+}
remowt/crates/remowt-endpoints/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-endpoints/src/lib.rs
@@ -0,0 +1,7 @@
+pub mod forward;
+pub mod fs;
+pub mod iroh_tunnel;
+pub mod nix_daemon;
+pub mod pty;
+pub mod subprocess;
+pub mod systemd;
remowt/crates/remowt-endpoints/src/nix_daemon.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-endpoints/src/nix_daemon.rs
@@ -0,0 +1,59 @@
+use std::process::Stdio;
+use std::sync::Arc;
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use remowt_link_shared::iroh_tunnel::{TunnelAddr, TunnelDialer};
+use serde::{Deserialize, Serialize};
+use std::result::Result;
+use tokio::process::Command;
+
+#[derive(Clone)]
+pub struct NixDaemon {
+ dialer: Arc<TunnelDialer>,
+}
+
+impl NixDaemon {
+ pub fn new(dialer: Arc<TunnelDialer>) -> Self {
+ Self { dialer }
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("nix daemon unavailable: {0}")]
+ DaemonUnavailable(String),
+ #[error("tunnel socket unavailable: {0}")]
+ Tunnel(String),
+}
+
+#[endpoints(ns = 4)]
+impl NixDaemon {
+ #[endpoints(id = 2)]
+ async fn serve_store(&self, store: String, tunnel: TunnelAddr) -> Result<(), Error> {
+ let mut child = Command::new("nix-daemon")
+ .arg("--stdio")
+ .arg("--store")
+ .arg(&store)
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .spawn()
+ .map_err(|e| Error::DaemonUnavailable(e.to_string()))?;
+ let tunnel = self
+ .dialer
+ .connect_tunnel(&tunnel)
+ .await
+ .map_err(|e| Error::Tunnel(e.to_string()))?;
+ let mut stdin = child.stdin.take().expect("piped");
+ let mut stdout = child.stdout.take().expect("piped");
+ tokio::spawn(async move {
+ let (mut tr, mut tw) = tokio::io::split(tunnel);
+ let _ = tokio::join!(
+ tokio::io::copy(&mut tr, &mut stdin),
+ tokio::io::copy(&mut stdout, &mut tw),
+ );
+ let _ = child.wait().await;
+ });
+ Ok(())
+ }
+}
remowt/crates/remowt-endpoints/src/pty.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-endpoints/src/pty.rs
@@ -0,0 +1,260 @@
+use std::collections::HashMap;
+use std::io;
+use std::os::fd::{AsRawFd, OwnedFd};
+use std::pin::Pin;
+use std::process::Stdio;
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll};
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use nix::libc;
+use nix::pty::{openpty, OpenptyResult, Winsize};
+use remowt_link_shared::iroh_tunnel::{TunnelAddr, TunnelDialer};
+use serde::{Deserialize, Serialize};
+use tokio::io::unix::AsyncFd;
+use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
+use tracing::{debug, info, warn};
+
+pub type ShellId = u64;
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("openpty failed: {0}")]
+ Open(String),
+ #[error("failed to spawn shell: {0}")]
+ Spawn(String),
+ #[error("failed to connect to forwarded socket: {0}")]
+ Connect(String),
+ #[error("no shell with that id")]
+ NoSuchShell,
+ #[error("resize failed: {0}")]
+ Resize(String),
+ #[error("io error: {0}")]
+ Io(String),
+}
+
+impl From<io::Error> for Error {
+ fn from(e: io::Error) -> Self {
+ Error::Io(e.to_string())
+ }
+}
+
+#[derive(Clone)]
+pub struct Pty {
+ shells: Arc<Mutex<HashMap<ShellId, OwnedFd>>>,
+ next_id: Arc<AtomicU64>,
+ dialer: Arc<TunnelDialer>,
+}
+
+impl Pty {
+ pub fn new(dialer: Arc<TunnelDialer>) -> Self {
+ Self {
+ shells: Default::default(),
+ next_id: Default::default(),
+ dialer,
+ }
+ }
+}
+
+#[endpoints(ns = 7)]
+impl Pty {
+ #[endpoints(id = 1)]
+ async fn open_shell(
+ &self,
+ tunnel: TunnelAddr,
+ term: String,
+ cols: u16,
+ rows: u16,
+ ) -> Result<ShellId, Error> {
+ let ws = Winsize {
+ ws_row: rows,
+ ws_col: cols,
+ ws_xpixel: 0,
+ ws_ypixel: 0,
+ };
+ let OpenptyResult { master, slave } =
+ openpty(Some(&ws), None).map_err(|e| Error::Open(e.to_string()))?;
+
+ let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_owned());
+
+ let slave_in = slave.try_clone()?;
+ let slave_out = slave.try_clone()?;
+ let slave_err = slave;
+
+ let mut cmd = tokio::process::Command::new(&shell);
+ cmd.env("TERM", &term);
+ if let Ok(home) = std::env::var("HOME") {
+ cmd.current_dir(home);
+ }
+ cmd.stdin(Stdio::from(slave_in));
+ cmd.stdout(Stdio::from(slave_out));
+ cmd.stderr(Stdio::from(slave_err));
+ // SAFETY: only async-signal-safe calls (setsid, ioctl) before exec.
+ unsafe {
+ cmd.pre_exec(|| {
+ nix::unistd::setsid().map_err(|e| io::Error::from_raw_os_error(e as i32))?;
+ if libc::ioctl(0, libc::TIOCSCTTY as _, 0) < 0 {
+ return Err(io::Error::last_os_error());
+ }
+ Ok(())
+ });
+ }
+
+ let mut child = cmd.spawn().map_err(|e| Error::Spawn(e.to_string()))?;
+
+ let resize_fd = master.try_clone()?;
+ let id = self.next_id.fetch_add(1, Ordering::Relaxed);
+ self.shells
+ .lock()
+ .expect("not poisoned")
+ .insert(id, resize_fd);
+
+ let sock = match self.dialer.connect_tunnel(&tunnel).await {
+ Ok(s) => s,
+ Err(e) => {
+ self.shells.lock().expect("not poisoned").remove(&id);
+ let _ = child.kill().await;
+ return Err(Error::Connect(e.to_string()));
+ }
+ };
+ let pty = AsyncPty::new(master)?;
+
+ debug!(id, shell, "shell opened");
+ let shells = self.shells.clone();
+ tokio::spawn(async move {
+ let mut pty = pty;
+ let mut sock = sock;
+ if let Err(e) = tokio::io::copy_bidirectional(&mut pty, &mut sock).await {
+ warn!(id, "shell pump ended: {e}");
+ }
+ let _ = child.kill().await;
+ shells.lock().expect("not poisoned").remove(&id);
+ info!(id, "shell closed");
+ });
+
+ Ok(id)
+ }
+
+ #[endpoints(id = 2)]
+ async fn resize(&self, id: ShellId, cols: u16, rows: u16) -> Result<(), Error> {
+ let ws = libc::winsize {
+ ws_row: rows,
+ ws_col: cols,
+ ws_xpixel: 0,
+ ws_ypixel: 0,
+ };
+ let shells = self.shells.lock().expect("not poisoned");
+ let fd = shells.get(&id).ok_or(Error::NoSuchShell)?;
+ // SAFETY: `fd` is a live PTY master
+ let rc = unsafe { libc::ioctl(fd.as_raw_fd(), libc::TIOCSWINSZ as _, &ws) };
+ if rc < 0 {
+ return Err(Error::Resize(io::Error::last_os_error().to_string()));
+ }
+ Ok(())
+ }
+}
+
+struct AsyncPty {
+ fd: AsyncFd<OwnedFd>,
+}
+
+impl AsyncPty {
+ fn new(fd: OwnedFd) -> io::Result<Self> {
+ let raw = fd.as_raw_fd();
+ // SAFETY: standard F_GETFL/F_SETFL round-trip on a valid fd.
+ unsafe {
+ let flags = libc::fcntl(raw, libc::F_GETFL);
+ if flags < 0 {
+ return Err(io::Error::last_os_error());
+ }
+ if libc::fcntl(raw, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
+ return Err(io::Error::last_os_error());
+ }
+ }
+ Ok(Self {
+ fd: AsyncFd::new(fd)?,
+ })
+ }
+}
+
+impl AsyncRead for AsyncPty {
+ fn poll_read(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &mut ReadBuf<'_>,
+ ) -> Poll<io::Result<()>> {
+ let this = self.get_mut();
+ loop {
+ let mut guard = match this.fd.poll_read_ready(cx) {
+ Poll::Ready(Ok(g)) => g,
+ Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
+ Poll::Pending => return Poll::Pending,
+ };
+ let unfilled = buf.initialize_unfilled();
+ let res = guard.try_io(|inner| {
+ let fd = inner.get_ref().as_raw_fd();
+ // SAFETY: writing into `unfilled`'s own backing storage.
+ let n = unsafe { libc::read(fd, unfilled.as_mut_ptr().cast(), unfilled.len()) };
+ if n < 0 {
+ let err = io::Error::last_os_error();
+ if err.raw_os_error() == Some(libc::EIO) {
+ Ok(0)
+ } else {
+ Err(err)
+ }
+ } else {
+ Ok(n as usize)
+ }
+ });
+ match res {
+ Ok(Ok(n)) => {
+ buf.advance(n);
+ return Poll::Ready(Ok(()));
+ }
+ Ok(Err(e)) => return Poll::Ready(Err(e)),
+ Err(_would_block) => continue,
+ }
+ }
+ }
+}
+
+impl AsyncWrite for AsyncPty {
+ fn poll_write(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &[u8],
+ ) -> Poll<io::Result<usize>> {
+ let this = self.get_mut();
+ loop {
+ let mut guard = match this.fd.poll_write_ready(cx) {
+ Poll::Ready(Ok(g)) => g,
+ Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
+ Poll::Pending => return Poll::Pending,
+ };
+ let res = guard.try_io(|inner| {
+ let fd = inner.get_ref().as_raw_fd();
+ // SAFETY: reading from `buf` for `buf.len()` bytes.
+ let n = unsafe { libc::write(fd, buf.as_ptr().cast(), buf.len()) };
+ if n < 0 {
+ Err(io::Error::last_os_error())
+ } else {
+ Ok(n as usize)
+ }
+ });
+ match res {
+ Ok(r) => return Poll::Ready(r),
+ Err(_would_block) => continue,
+ }
+ }
+ }
+
+ fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ Poll::Ready(Ok(()))
+ }
+
+ fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ Poll::Ready(Ok(()))
+ }
+}
remowt/crates/remowt-endpoints/src/subprocess.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-endpoints/src/subprocess.rs
@@ -0,0 +1,293 @@
+use std::collections::HashMap;
+use std::io;
+use std::process::Stdio;
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::{Arc, Mutex};
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use camino::Utf8PathBuf;
+use nix::sys::signal::{self, Signal};
+use nix::unistd::Pid;
+use remowt_link_shared::iroh_tunnel::{TunnelAddr, TunnelDialer, TunnelStream};
+use serde::{Deserialize, Serialize};
+use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
+use tokio::process::{ChildStderr, ChildStdout, Command};
+use tokio::sync::{mpsc, watch};
+use tracing::{debug, warn};
+
+pub type ProcId = u64;
+
+#[derive(Serialize, Deserialize, Debug)]
+pub enum StdioSpec {
+ Null,
+ Tunnel(TunnelAddr),
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub enum StderrSpec {
+ Null,
+ Tunnel(TunnelAddr),
+ MergeWithStdout,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct SpawnSpec {
+ pub program: String,
+ pub args: Vec<String>,
+ pub env: Vec<(String, String)>,
+ pub env_clear: bool,
+ pub cwd: Option<Utf8PathBuf>,
+ pub stdin: StdioSpec,
+ pub stdout: StdioSpec,
+ pub stderr: StderrSpec,
+}
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("spawn failed: {0}")]
+ Spawn(String),
+ #[error("connect to forwarded socket failed: {0}")]
+ Connect(String),
+ #[error("no process with that id")]
+ NoSuchProcess,
+ #[error("MergeWithStdout requires stdout=Socket")]
+ BadMerge,
+ #[error("invalid signal: {0}")]
+ BadSignal(i32),
+ #[error("kill failed: {0}")]
+ Kill(String),
+ #[error("io error: {0}")]
+ Io(String),
+}
+
+impl From<io::Error> for Error {
+ fn from(e: io::Error) -> Self {
+ Error::Io(e.to_string())
+ }
+}
+
+struct ChildState {
+ pid: u32,
+ exit_rx: watch::Receiver<Option<Option<i32>>>,
+}
+
+#[derive(Clone)]
+pub struct Subprocess {
+ children: Arc<Mutex<HashMap<ProcId, ChildState>>>,
+ next_id: Arc<AtomicU64>,
+ dialer: Arc<TunnelDialer>,
+}
+
+impl Subprocess {
+ pub fn new(dialer: Arc<TunnelDialer>) -> Self {
+ Self {
+ children: Default::default(),
+ next_id: Default::default(),
+ dialer,
+ }
+ }
+}
+
+#[endpoints(ns = 10)]
+impl Subprocess {
+ #[endpoints(id = 1)]
+ async fn spawn(&self, spec: SpawnSpec) -> Result<ProcId, Error> {
+ let SpawnSpec {
+ program,
+ args,
+ env,
+ env_clear,
+ cwd,
+ stdin,
+ stdout,
+ stderr,
+ } = spec;
+
+ if matches!(stderr, StderrSpec::MergeWithStdout) && !matches!(stdout, StdioSpec::Tunnel(_))
+ {
+ return Err(Error::BadMerge);
+ }
+
+ let mut cmd = Command::new(&program);
+ cmd.args(&args);
+ if env_clear {
+ cmd.env_clear();
+ }
+ for (k, v) in &env {
+ cmd.env(k, v);
+ }
+ if let Some(cwd) = &cwd {
+ cmd.current_dir(cwd);
+ }
+ cmd.stdin(match &stdin {
+ StdioSpec::Tunnel(_) => Stdio::piped(),
+ StdioSpec::Null => Stdio::null(),
+ });
+ cmd.stdout(match &stdout {
+ StdioSpec::Tunnel(_) => Stdio::piped(),
+ StdioSpec::Null => Stdio::null(),
+ });
+ cmd.stderr(match &stderr {
+ StderrSpec::Tunnel(_) | StderrSpec::MergeWithStdout => Stdio::piped(),
+ StderrSpec::Null => Stdio::null(),
+ });
+ cmd.kill_on_drop(false);
+
+ let mut child = cmd.spawn().map_err(|e| Error::Spawn(e.to_string()))?;
+ let pid = child
+ .id()
+ .ok_or_else(|| Error::Spawn("child exited before pid available".to_owned()))?;
+
+ if let StdioSpec::Tunnel(addr) = &stdin {
+ let sock = self
+ .dialer
+ .connect_tunnel(addr)
+ .await
+ .map_err(|e| Error::Connect(e.to_string()))?;
+ let mut stdin_w = child.stdin.take().expect("piped");
+ tokio::spawn(async move {
+ let (mut sr, _) = tokio::io::split(sock);
+ let _ = tokio::io::copy(&mut sr, &mut stdin_w).await;
+ let _ = stdin_w.shutdown().await;
+ });
+ }
+
+ let stdout_handle = child.stdout.take();
+ let stderr_handle = child.stderr.take();
+
+ match (&stdout, &stderr, stdout_handle, stderr_handle) {
+ (StdioSpec::Tunnel(out_addr), StderrSpec::MergeWithStdout, Some(out), Some(err)) => {
+ let sock = self
+ .dialer
+ .connect_tunnel(out_addr)
+ .await
+ .map_err(|e| Error::Connect(e.to_string()))?;
+ tokio::spawn(merge_to_sock(out, err, sock));
+ }
+ (StdioSpec::Tunnel(out_addr), _, Some(out), err_opt) => {
+ let sock = self
+ .dialer
+ .connect_tunnel(out_addr)
+ .await
+ .map_err(|e| Error::Connect(e.to_string()))?;
+ tokio::spawn(pump_to_sock(out, sock));
+ if let (StderrSpec::Tunnel(err_addr), Some(err)) = (&stderr, err_opt) {
+ let err_sock = self
+ .dialer
+ .connect_tunnel(err_addr)
+ .await
+ .map_err(|e| Error::Connect(e.to_string()))?;
+ tokio::spawn(pump_to_sock(err, err_sock));
+ }
+ }
+ (StdioSpec::Null, StderrSpec::Tunnel(err_addr), _, Some(err)) => {
+ let sock = self
+ .dialer
+ .connect_tunnel(err_addr)
+ .await
+ .map_err(|e| Error::Connect(e.to_string()))?;
+ tokio::spawn(pump_to_sock(err, sock));
+ }
+ _ => {}
+ }
+
+ let (exit_tx, exit_rx) = watch::channel(None);
+ let id = self.next_id.fetch_add(1, Ordering::Relaxed);
+ self.children
+ .lock()
+ .expect("not poisoned")
+ .insert(id, ChildState { pid, exit_rx });
+
+ debug!(id, pid, program, "subprocess spawned");
+ tokio::spawn(async move {
+ let result = child.wait().await;
+ let code = match result {
+ Ok(status) => status.code(),
+ Err(e) => {
+ warn!(id, "child.wait failed: {e}");
+ None
+ }
+ };
+ let _ = exit_tx.send(Some(code));
+ });
+
+ Ok(id)
+ }
+
+ #[endpoints(id = 2)]
+ async fn wait(&self, id: ProcId) -> Result<Option<i32>, Error> {
+ let mut rx = {
+ let map = self.children.lock().expect("not poisoned");
+ let entry = map.get(&id).ok_or(Error::NoSuchProcess)?;
+ entry.exit_rx.clone()
+ };
+ rx.wait_for(|v| v.is_some())
+ .await
+ .map_err(|_| Error::Io("exit channel closed".to_owned()))?;
+ let code = rx.borrow().flatten();
+ self.children.lock().expect("not poisoned").remove(&id);
+ Ok(code)
+ }
+
+ #[endpoints(id = 3)]
+ async fn kill(&self, id: ProcId, signal: i32) -> Result<(), Error> {
+ let pid = {
+ let map = self.children.lock().expect("not poisoned");
+ let entry = map.get(&id).ok_or(Error::NoSuchProcess)?;
+ entry.pid
+ };
+ let sig = Signal::try_from(signal).map_err(|_| Error::BadSignal(signal))?;
+ signal::kill(Pid::from_raw(pid as i32), sig).map_err(|e| Error::Kill(e.to_string()))?;
+ Ok(())
+ }
+}
+
+async fn pump_to_sock<R>(mut from: R, sock: TunnelStream)
+where
+ R: tokio::io::AsyncRead + Unpin,
+{
+ let (_, mut sw) = tokio::io::split(sock);
+ let _ = tokio::io::copy(&mut from, &mut sw).await;
+ let _ = sw.shutdown().await;
+}
+
+async fn merge_to_sock(mut stdout: ChildStdout, mut stderr: ChildStderr, sock: TunnelStream) {
+ let (_, mut sw) = tokio::io::split(sock);
+ let (tx, mut rx) = mpsc::channel::<Vec<u8>>(64);
+ let tx_out = tx.clone();
+ let out_pump = tokio::spawn(async move {
+ let mut buf = vec![0u8; 4096];
+ loop {
+ match stdout.read(&mut buf).await {
+ Ok(0) | Err(_) => break,
+ Ok(n) => {
+ if tx_out.send(buf[..n].to_vec()).await.is_err() {
+ break;
+ }
+ }
+ }
+ }
+ });
+ let err_pump = tokio::spawn(async move {
+ let mut buf = vec![0u8; 4096];
+ loop {
+ match stderr.read(&mut buf).await {
+ Ok(0) | Err(_) => break,
+ Ok(n) => {
+ if tx.send(buf[..n].to_vec()).await.is_err() {
+ break;
+ }
+ }
+ }
+ }
+ });
+ while let Some(chunk) = rx.recv().await {
+ if sw.write_all(&chunk).await.is_err() {
+ break;
+ }
+ }
+ let _ = out_pump.await;
+ let _ = err_pump.await;
+ let _ = sw.shutdown().await;
+}
remowt/crates/remowt-endpoints/src/systemd.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-endpoints/src/systemd.rs
@@ -0,0 +1,54 @@
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use serde::{Deserialize, Serialize};
+use zbus::proxy;
+use zbus::zvariant::OwnedObjectPath;
+
+pub struct Systemd;
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("systemd request failed: {0}")]
+ Failed(String),
+}
+
+#[proxy(
+ interface = "org.freedesktop.systemd1.Manager",
+ default_service = "org.freedesktop.systemd1",
+ default_path = "/org/freedesktop/systemd1"
+)]
+trait Manager {
+ fn start_unit(&self, name: &str, mode: &str) -> zbus::Result<OwnedObjectPath>;
+ fn stop_unit(&self, name: &str, mode: &str) -> zbus::Result<OwnedObjectPath>;
+}
+
+async fn manager() -> Result<ManagerProxy<'static>, Error> {
+ let conn = zbus::Connection::system()
+ .await
+ .map_err(|e| Error::Failed(e.to_string()))?;
+ ManagerProxy::new(&conn)
+ .await
+ .map_err(|e| Error::Failed(e.to_string()))
+}
+
+#[endpoints(ns = 5)]
+impl Systemd {
+ #[endpoints(id = 1)]
+ async fn start(&self, unit: String) -> Result<(), Error> {
+ manager()
+ .await?
+ .start_unit(&unit, "replace")
+ .await
+ .map_err(|e| Error::Failed(e.to_string()))?;
+ Ok(())
+ }
+ #[endpoints(id = 2)]
+ async fn stop(&self, unit: String) -> Result<(), Error> {
+ manager()
+ .await?
+ .stop_unit(&unit, "replace")
+ .await
+ .map_err(|e| Error::Failed(e.to_string()))?;
+ Ok(())
+ }
+}
remowt/crates/remowt-link-shared/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-link-shared/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "remowt-link-shared"
+description = "Shared bifrostlink endpoint wiring for remowt"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+bifrostlink.workspace = true
+bytes.workspace = true
+camino = { workspace = true }
+serde = { workspace = true, features = ["derive"] }
+serde_json.workspace = true
+thiserror.workspace = true
+tokio = { workspace = true, features = ["fs", "io-util", "macros", "net", "sync", "rt"] }
+tokio-util = { workspace = true, features = ["codec"] }
+tracing.workspace = true
+remowt-ui-prompt.workspace = true
+
+iroh.workspace = true
+iroh-base.workspace = true
+noq-udp.workspace = true
+n0-watcher.workspace = true
+futures.workspace = true
remowt/crates/remowt-link-shared/src/editor.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-link-shared/src/editor.rs
@@ -0,0 +1,45 @@
+use std::future::Future;
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::{Config, Rpc};
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("editor failed: {0}")]
+ Failed(String),
+}
+
+pub trait EditorBackend: Send + Sync {
+ fn open_editor(&self, socket_path: String) -> impl Future<Output = Result<(), Error>> + Send;
+ fn expose_tcp(&self, addr: String) -> impl Future<Output = Result<u16, Error>> + Send;
+ fn expose_udp(&self, addr: String) -> impl Future<Output = Result<u16, Error>> + Send;
+}
+
+pub struct EditorEndpoints<E>(pub E);
+
+#[endpoints(ns = 8)]
+impl<E: EditorBackend + 'static> EditorEndpoints<E> {
+ #[endpoints(id = 1)]
+ async fn open_editor(&self, socket_path: String) -> Result<(), Error> {
+ self.0.open_editor(socket_path).await
+ }
+
+ #[endpoints(id = 2)]
+ async fn expose_tcp(&self, addr: String) -> Result<u16, Error> {
+ self.0.expose_tcp(addr).await
+ }
+
+ #[endpoints(id = 3)]
+ async fn expose_udp(&self, addr: String) -> Result<u16, Error> {
+ self.0.expose_udp(addr).await
+ }
+}
+
+pub fn serve_editor<E, C>(rpc: &mut Rpc<C>, editor: E)
+where
+ E: EditorBackend + Send + Sync + 'static,
+ C: Config,
+{
+ EditorEndpoints(editor).register_endpoints(rpc);
+}
remowt/crates/remowt-link-shared/src/iroh_tunnel.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-link-shared/src/iroh_tunnel.rs
@@ -0,0 +1,536 @@
+use std::collections::HashMap;
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll};
+use std::time::Duration;
+use std::{fmt, io};
+
+use anyhow::Result;
+use bytes::{Bytes, BytesMut};
+use camino::Utf8PathBuf;
+use futures::{SinkExt as _, Stream};
+use iroh::endpoint::transports::{
+ CustomEndpoint, CustomSender, CustomTransport, RecvInfo, Transmit,
+};
+use iroh::endpoint::{presets, Connection, RecvStream, SendStream};
+use iroh::{Endpoint, EndpointId, SecretKey};
+use iroh_base::CustomAddr;
+use serde::{Deserialize, Serialize};
+use tokio::io::{AsyncRead, AsyncWrite, ReadBuf, ReadHalf, WriteHalf};
+use tokio::net::UnixStream;
+use tokio::sync::{mpsc, watch};
+use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec};
+use tracing::{debug, warn};
+
+// "rmwtssh"
+pub const REMOWT_SSH_TRANSPORT_ID: u64 = 0x_72_6d_77_74_73_73_68;
+
+pub const REMOWT_ALPN: &[u8] = b"remowt/tunnel/0";
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub enum TunnelAddr {
+ Unix(Utf8PathBuf),
+ Iroh { token: u64 },
+}
+
+pub fn ssh_custom_addr(id: EndpointId) -> CustomAddr {
+ CustomAddr::from((REMOWT_SSH_TRANSPORT_ID, &id.as_bytes()[..]))
+}
+
+pub async fn build_endpoint<S>(
+ secret: SecretKey,
+ stream: S,
+ remote: EndpointId,
+ accept: bool,
+) -> Result<Endpoint>
+where
+ S: AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static,
+{
+ let local = secret.public();
+ let transport = Arc::new(SshTransport::new(stream, local, remote));
+ let mut builder = Endpoint::builder(presets::N0)
+ .secret_key(secret)
+ .add_custom_transport(transport);
+ if accept {
+ builder = builder.alpns(vec![REMOWT_ALPN.to_vec()]);
+ }
+ Ok(builder.bind().await?)
+}
+
+struct SshTransport {
+ local: CustomAddr,
+ remote: CustomAddr,
+ addrs: n0_watcher::Watchable<Vec<CustomAddr>>,
+ endpoint: Mutex<Option<SshEndpointParts>>,
+ sender: Arc<SshSender>,
+}
+
+struct SshEndpointParts {
+ reader: Pin<Box<dyn FramedDatagrams>>,
+}
+
+trait FramedDatagrams: Stream<Item = io::Result<BytesMut>> + Send + Sync {}
+impl<T: Stream<Item = io::Result<BytesMut>> + Send + Sync> FramedDatagrams for T {}
+
+impl fmt::Debug for SshTransport {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("SshTransport").finish_non_exhaustive()
+ }
+}
+
+impl SshTransport {
+ fn new<S>(stream: S, local: EndpointId, remote: EndpointId) -> Self
+ where
+ S: AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static,
+ {
+ let (read, write): (ReadHalf<S>, WriteHalf<S>) = tokio::io::split(stream);
+ let reader = FramedRead::new(read, LengthDelimitedCodec::new());
+
+ let (tx, rx) = mpsc::unbounded_channel::<Bytes>();
+ tokio::spawn(writer_task(rx, write));
+
+ let local = ssh_custom_addr(local);
+ Self {
+ addrs: n0_watcher::Watchable::new(vec![local.clone()]),
+ local,
+ remote: ssh_custom_addr(remote),
+ endpoint: Mutex::new(Some(SshEndpointParts {
+ reader: Box::pin(reader),
+ })),
+ sender: Arc::new(SshSender { tx }),
+ }
+ }
+}
+
+async fn writer_task<W: AsyncWrite + Unpin>(mut rx: mpsc::UnboundedReceiver<Bytes>, write: W) {
+ let mut framed = FramedWrite::new(write, LengthDelimitedCodec::new());
+ while let Some(datagram) = rx.recv().await {
+ if let Err(e) = framed.send(datagram).await {
+ debug!("ssh transport writer ended: {e}");
+ break;
+ }
+ }
+}
+
+impl CustomTransport for SshTransport {
+ fn bind(&self) -> io::Result<Box<dyn CustomEndpoint>> {
+ let parts = self
+ .endpoint
+ .lock()
+ .expect("not poisoned")
+ .take()
+ .ok_or_else(|| io::Error::other("ssh transport already bound"))?;
+ Ok(Box::new(SshEndpoint {
+ local: self.local.clone(),
+ remote: self.remote.clone(),
+ addrs: self.addrs.clone(),
+ sender: self.sender.clone(),
+ reader: parts.reader,
+ }))
+ }
+}
+
+struct SshEndpoint {
+ local: CustomAddr,
+ remote: CustomAddr,
+ addrs: n0_watcher::Watchable<Vec<CustomAddr>>,
+ sender: Arc<SshSender>,
+ reader: Pin<Box<dyn FramedDatagrams>>,
+}
+
+impl fmt::Debug for SshEndpoint {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("SshEndpoint").finish_non_exhaustive()
+ }
+}
+
+impl CustomEndpoint for SshEndpoint {
+ fn watch_local_addrs(&self) -> n0_watcher::Direct<Vec<CustomAddr>> {
+ self.addrs.watch()
+ }
+
+ fn create_sender(&self) -> Arc<dyn CustomSender> {
+ self.sender.clone()
+ }
+
+ fn poll_recv(
+ &mut self,
+ cx: &mut Context,
+ bufs: &mut [io::IoSliceMut<'_>],
+ metas: &mut [noq_udp::RecvMeta],
+ recv_infos: &mut [RecvInfo],
+ ) -> Poll<io::Result<usize>> {
+ assert_eq!(bufs.len(), metas.len());
+ assert_eq!(bufs.len(), recv_infos.len());
+ if bufs.is_empty() {
+ return Poll::Ready(Ok(0));
+ }
+ let mut count = 0;
+ while count < bufs.len() {
+ match self.reader.as_mut().poll_next(cx) {
+ Poll::Ready(Some(Ok(frame))) => {
+ let buf = &mut bufs[count];
+ if buf.len() < frame.len() {
+ warn!("ssh transport datagram {} > buf {}", frame.len(), buf.len());
+ if count > 0 {
+ break;
+ }
+ return Poll::Ready(Err(io::Error::other("datagram exceeds recv buffer")));
+ }
+ buf[..frame.len()].copy_from_slice(&frame);
+ metas[count].len = frame.len();
+ metas[count].stride = frame.len();
+ recv_infos[count] =
+ RecvInfo::new(self.remote.clone(), Some(self.local.clone()));
+ count += 1;
+ }
+ Poll::Ready(Some(Err(e))) => {
+ if count > 0 {
+ break;
+ }
+ return Poll::Ready(Err(e));
+ }
+ Poll::Ready(None) => {
+ if count > 0 {
+ break;
+ }
+ return Poll::Ready(Err(io::Error::other("ssh transport closed")));
+ }
+ Poll::Pending => {
+ if count > 0 {
+ break;
+ }
+ return Poll::Pending;
+ }
+ }
+ }
+ Poll::Ready(Ok(count))
+ }
+}
+
+#[derive(Debug)]
+struct SshSender {
+ tx: mpsc::UnboundedSender<Bytes>,
+}
+
+impl CustomSender for SshSender {
+ fn is_valid_send_addr(&self, addr: &CustomAddr) -> bool {
+ addr.id() == REMOWT_SSH_TRANSPORT_ID
+ }
+
+ fn poll_send(
+ &self,
+ _cx: &mut Context,
+ _dst: &CustomAddr,
+ _src: Option<&CustomAddr>,
+ transmit: &Transmit<'_>,
+ ) -> Poll<io::Result<()>> {
+ let segment = transmit.segment_size.unwrap_or(transmit.contents.len());
+ if segment == 0 || transmit.contents.is_empty() {
+ return Poll::Ready(Ok(()));
+ }
+ for chunk in transmit.contents.chunks(segment) {
+ if self.tx.send(Bytes::copy_from_slice(chunk)).is_err() {
+ return Poll::Ready(Err(io::Error::other("ssh transport writer gone")));
+ }
+ }
+ Poll::Ready(Ok(()))
+ }
+}
+
+pub struct IrohBiStream {
+ send: SendStream,
+ recv: RecvStream,
+}
+
+impl IrohBiStream {
+ pub fn new(send: SendStream, recv: RecvStream) -> Self {
+ Self { send, recv }
+ }
+}
+
+impl AsyncRead for IrohBiStream {
+ fn poll_read(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &mut ReadBuf<'_>,
+ ) -> Poll<io::Result<()>> {
+ AsyncRead::poll_read(Pin::new(&mut self.get_mut().recv), cx, buf)
+ }
+}
+
+impl AsyncWrite for IrohBiStream {
+ fn poll_write(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &[u8],
+ ) -> Poll<io::Result<usize>> {
+ AsyncWrite::poll_write(Pin::new(&mut self.get_mut().send), cx, buf)
+ }
+ fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ AsyncWrite::poll_flush(Pin::new(&mut self.get_mut().send), cx)
+ }
+ fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ AsyncWrite::poll_shutdown(Pin::new(&mut self.get_mut().send), cx)
+ }
+}
+
+pub enum TunnelStream {
+ Unix(UnixStream),
+ Iroh(IrohBiStream),
+}
+
+impl AsyncRead for TunnelStream {
+ fn poll_read(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &mut ReadBuf<'_>,
+ ) -> Poll<io::Result<()>> {
+ match self.get_mut() {
+ TunnelStream::Unix(s) => Pin::new(s).poll_read(cx, buf),
+ TunnelStream::Iroh(s) => Pin::new(s).poll_read(cx, buf),
+ }
+ }
+}
+
+impl AsyncWrite for TunnelStream {
+ fn poll_write(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &[u8],
+ ) -> Poll<io::Result<usize>> {
+ match self.get_mut() {
+ TunnelStream::Unix(s) => Pin::new(s).poll_write(cx, buf),
+ TunnelStream::Iroh(s) => Pin::new(s).poll_write(cx, buf),
+ }
+ }
+ fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ match self.get_mut() {
+ TunnelStream::Unix(s) => Pin::new(s).poll_flush(cx),
+ TunnelStream::Iroh(s) => Pin::new(s).poll_flush(cx),
+ }
+ }
+ fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ match self.get_mut() {
+ TunnelStream::Unix(s) => Pin::new(s).poll_shutdown(cx),
+ TunnelStream::Iroh(s) => Pin::new(s).poll_shutdown(cx),
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct TunnelDialer {
+ conn: watch::Sender<Option<Connection>>,
+ router: Arc<Mutex<Option<Arc<DatagramRouter>>>>,
+}
+
+impl Default for TunnelDialer {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl TunnelDialer {
+ pub fn new() -> Self {
+ let (conn, _) = watch::channel(None);
+ Self {
+ conn,
+ router: Arc::new(Mutex::new(None)),
+ }
+ }
+
+ pub fn set_conn(&self, conn: Connection) {
+ *self.router.lock().expect("lock") = Some(DatagramRouter::spawn(conn.clone()));
+ self.conn.send_replace(Some(conn));
+ }
+
+ pub fn router(&self) -> Option<Arc<DatagramRouter>> {
+ self.router.lock().expect("lock").clone()
+ }
+
+ pub async fn connect_tunnel(&self, addr: &TunnelAddr) -> io::Result<TunnelStream> {
+ match addr {
+ TunnelAddr::Unix(path) => Ok(TunnelStream::Unix(UnixStream::connect(path).await?)),
+ TunnelAddr::Iroh { token } => {
+ let mut rx = self.conn.subscribe();
+ let conn =
+ tokio::time::timeout(Duration::from_secs(5), rx.wait_for(|c| c.is_some()))
+ .await
+ .map_err(|_| io::Error::other("timed out waiting for iroh connection"))?
+ .map_err(|_| io::Error::other("iroh connection channel closed"))?
+ .clone()
+ .expect("is_some");
+ let (mut send, recv) = conn.open_bi().await.map_err(io::Error::other)?;
+ send.write_all(&token.to_be_bytes())
+ .await
+ .map_err(io::Error::other)?;
+ Ok(TunnelStream::Iroh(IrohBiStream::new(send, recv)))
+ }
+ }
+ }
+}
+
+const DGRAM_HEADER: usize = 16;
+
+type DatagramRoutes = Arc<Mutex<HashMap<u64, mpsc::UnboundedSender<(u64, Bytes)>>>>;
+
+pub struct DatagramRouter {
+ conn: Connection,
+ routes: DatagramRoutes,
+}
+
+impl DatagramRouter {
+ pub fn spawn(conn: Connection) -> Arc<Self> {
+ let routes: DatagramRoutes = Arc::new(Mutex::new(HashMap::new()));
+ let read_routes = routes.clone();
+ let read_conn = conn.clone();
+ tokio::spawn(async move {
+ loop {
+ match read_conn.read_datagram().await {
+ Ok(dg) if dg.len() >= DGRAM_HEADER => {
+ let session = u64::from_be_bytes(dg[0..8].try_into().expect("8 bytes"));
+ let sub = u64::from_be_bytes(dg[8..16].try_into().expect("8 bytes"));
+ let payload = dg.slice(DGRAM_HEADER..);
+ let tx = read_routes.lock().expect("lock").get(&session).cloned();
+ if let Some(tx) = tx {
+ let _ = tx.send((sub, payload));
+ }
+ }
+ Ok(_) => {}
+ Err(e) => {
+ debug!("datagram read loop ended: {e}");
+ break;
+ }
+ }
+ }
+ });
+ Arc::new(Self { conn, routes })
+ }
+
+ pub fn register(&self, session: u64) -> mpsc::UnboundedReceiver<(u64, Bytes)> {
+ let (tx, rx) = mpsc::unbounded_channel();
+ self.routes.lock().expect("lock").insert(session, tx);
+ rx
+ }
+
+ pub fn unregister(&self, session: u64) {
+ self.routes.lock().expect("lock").remove(&session);
+ }
+
+ pub fn send(&self, session: u64, sub: u64, payload: &[u8]) -> io::Result<()> {
+ let mut buf = BytesMut::with_capacity(DGRAM_HEADER + payload.len());
+ buf.extend_from_slice(&session.to_be_bytes());
+ buf.extend_from_slice(&sub.to_be_bytes());
+ buf.extend_from_slice(payload);
+ self.conn
+ .send_datagram(buf.freeze())
+ .map_err(io::Error::other)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use iroh::endpoint::presets;
+ use iroh::{EndpointAddr, RelayMode, TransportAddr};
+
+ async fn endpoint<S>(secret: SecretKey, stream: S, remote: EndpointId, accept: bool) -> Endpoint
+ where
+ S: AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static,
+ {
+ let local = secret.public();
+ let transport = Arc::new(SshTransport::new(stream, local, remote));
+ let mut builder = Endpoint::builder(presets::N0)
+ .secret_key(secret)
+ .relay_mode(RelayMode::Disabled)
+ .clear_ip_transports()
+ .add_custom_transport(transport);
+ if accept {
+ builder = builder.alpns(vec![REMOWT_ALPN.to_vec()]);
+ }
+ builder.bind().await.expect("bind")
+ }
+
+ #[tokio::test]
+ async fn echo_over_ssh_transport() {
+ let (client_pipe, agent_pipe) = tokio::io::duplex(256 * 1024);
+ let client_secret = SecretKey::generate();
+ let agent_secret = SecretKey::generate();
+ let client_id = client_secret.public();
+ let agent_id = agent_secret.public();
+
+ let client = endpoint(client_secret, client_pipe, agent_id, false).await;
+ let agent = endpoint(agent_secret, agent_pipe, client_id, true).await;
+
+ let server = tokio::spawn(async move {
+ let incoming = agent.accept().await.expect("incoming");
+ let conn = incoming
+ .accept()
+ .expect("accept")
+ .await
+ .expect("connecting");
+ assert_eq!(conn.remote_id(), client_id);
+ let (mut send, mut recv) = conn.accept_bi().await.expect("accept_bi");
+ let mut buf = [0u8; 5];
+ recv.read_exact(&mut buf).await.expect("read");
+ send.write_all(&buf).await.expect("write");
+ send.finish().expect("finish");
+ conn.closed().await;
+ });
+
+ let addr =
+ EndpointAddr::from_parts(agent_id, [TransportAddr::Custom(ssh_custom_addr(agent_id))]);
+ let conn = client.connect(addr, REMOWT_ALPN).await.expect("connect");
+ assert_eq!(conn.remote_id(), agent_id);
+ let (mut send, mut recv) = conn.open_bi().await.expect("open_bi");
+ send.write_all(b"hello").await.expect("write");
+ send.finish().expect("finish");
+ let mut buf = [0u8; 5];
+ recv.read_exact(&mut buf).await.expect("read");
+ assert_eq!(&buf, b"hello");
+
+ conn.close(0u32.into(), b"done");
+ let _ = server.await;
+ }
+
+ #[tokio::test]
+ async fn dgram_round_trip() {
+ let (client_pipe, agent_pipe) = tokio::io::duplex(256 * 1024);
+ let client_secret = SecretKey::generate();
+ let agent_secret = SecretKey::generate();
+ let client_id = client_secret.public();
+ let agent_id = agent_secret.public();
+
+ let client = endpoint(client_secret, client_pipe, agent_id, false).await;
+ let agent = endpoint(agent_secret, agent_pipe, client_id, true).await;
+
+ let server = tokio::spawn(async move {
+ let incoming = agent.accept().await.expect("incoming");
+ let conn = incoming
+ .accept()
+ .expect("accept")
+ .await
+ .expect("connecting");
+ let router = DatagramRouter::spawn(conn);
+ let mut rx = router.register(7);
+ if let Some((sub, payload)) = rx.recv().await {
+ router.send(7, sub, &payload).expect("send reply");
+ }
+ tokio::time::sleep(Duration::from_millis(200)).await;
+ });
+
+ let addr =
+ EndpointAddr::from_parts(agent_id, [TransportAddr::Custom(ssh_custom_addr(agent_id))]);
+ let conn = client.connect(addr, REMOWT_ALPN).await.expect("connect");
+ let router = DatagramRouter::spawn(conn);
+ let mut rx = router.register(7);
+ router.send(7, 42, b"ping").expect("send");
+ let (sub, payload) = tokio::time::timeout(Duration::from_secs(5), rx.recv())
+ .await
+ .expect("datagram timed out")
+ .expect("router closed");
+ assert_eq!(sub, 42);
+ assert_eq!(&payload[..], b"ping");
+ let _ = server.await;
+ }
+}
remowt/crates/remowt-link-shared/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-link-shared/src/lib.rs
@@ -0,0 +1,89 @@
+use bifrostlink::error::{ErrorT, ListenerForYourRequestHasBeenDeadError, ResponseError};
+use bifrostlink::notification;
+use bifrostlink::packet::OpaquePacketWrapper;
+use bifrostlink::AddressT;
+use bytes::Bytes;
+use serde::de::DeserializeOwned;
+use serde::{Deserialize, Serialize};
+
+pub mod editor;
+pub mod iroh_tunnel;
+pub mod port;
+
+#[derive(Clone, Serialize, Hash, Eq, Debug, PartialEq, Deserialize)]
+pub enum Address {
+ User,
+ Agent,
+ AgentPrivileged,
+ Plugin(u16),
+}
+impl AddressT for Address {}
+
+pub mod plugin;
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error("listener is dead")]
+ ListenerDead,
+ #[error("response: {0}")]
+ Response(String),
+
+ #[error(transparent)]
+ Ui(#[from] remowt_ui_prompt::Error),
+}
+
+impl From<ListenerForYourRequestHasBeenDeadError> for Error {
+ fn from(_value: ListenerForYourRequestHasBeenDeadError) -> Self {
+ Self::ListenerDead
+ }
+}
+impl From<serde_json::Error> for Error {
+ fn from(_value: serde_json::Error) -> Self {
+ Self::ListenerDead
+ }
+}
+impl From<Error> for ResponseError {
+ fn from(val: Error) -> Self {
+ ResponseError(val.to_string())
+ }
+}
+impl From<ResponseError> for Error {
+ fn from(value: ResponseError) -> Self {
+ Self::Response(value.0)
+ }
+}
+impl ErrorT for Error {}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct TestNotification {
+ pub value: u32,
+}
+notification!((0x0100) TestNotification);
+
+pub struct BifConfig;
+impl bifrostlink::Config for BifConfig {
+ type Address = Address;
+
+ type Error = Error;
+
+ type EncodedData = Vec<u8>;
+
+ fn decode_headers(
+ data_with_headers: Bytes,
+ ) -> Result<(OpaquePacketWrapper<Self::Address>, Self::EncodedData), Self::Error> {
+ let (header, data): (OpaquePacketWrapper<Self::Address>, Vec<u8>) =
+ serde_json::from_slice(&data_with_headers)?;
+ Ok((header, data))
+ }
+
+ fn decode_data<T: DeserializeOwned>(data: Self::EncodedData) -> Result<T, Self::Error> {
+ let v: T = serde_json::from_slice(&data)?;
+ Ok(v)
+ }
+
+ fn encode_data<T: Serialize>(headers: OpaquePacketWrapper<Self::Address>, data: T) -> Bytes {
+ let data = serde_json::to_vec(&data).expect("serialization shouldn't fail");
+ let o = serde_json::to_vec(&(headers, data)).expect("serialization shouldn't fail");
+ o.into()
+ }
+}
remowt/crates/remowt-link-shared/src/plugin.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-link-shared/src/plugin.rs
@@ -0,0 +1,39 @@
+use std::future::Future;
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("plugin name must be a bare file name")]
+ BadName,
+ #[error("spawning plugin failed: {0}")]
+ Spawn(String),
+ #[error("agent is shutting down")]
+ Gone,
+}
+
+pub trait PluginHost: Send + Sync {
+ fn load_plugin(&self, id: u16, name: String) -> impl Future<Output = Result<(), Error>> + Send;
+
+ fn load_plugin_path(
+ &self,
+ id: u16,
+ path: String,
+ ) -> impl Future<Output = Result<(), Error>> + Send;
+}
+
+pub struct PluginEndpoints<H>(pub H);
+
+#[endpoints(ns = 9)]
+impl<H: PluginHost + 'static> PluginEndpoints<H> {
+ #[endpoints(id = 1)]
+ async fn load_plugin(&self, id: u16, name: String) -> Result<(), Error> {
+ self.0.load_plugin(id, name).await
+ }
+ #[endpoints(id = 2)]
+ async fn load_plugin_path(&self, id: u16, path: String) -> Result<(), Error> {
+ self.0.load_plugin_path(id, path).await
+ }
+}
remowt/crates/remowt-link-shared/src/port.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-link-shared/src/port.rs
@@ -0,0 +1,82 @@
+use std::io;
+
+use bifrostlink::Port;
+use bytes::{Bytes, BytesMut};
+use tokio::io::{AsyncRead, AsyncReadExt as _, AsyncWrite, AsyncWriteExt as _};
+use tokio::select;
+
+pub fn child_port<R, W>(mut reader: R, mut writer: W) -> Port
+where
+ R: AsyncRead + Unpin + Send + 'static,
+ W: AsyncWrite + Unpin + Send + 'static,
+{
+ Port::new(|mut rx, tx| async move {
+ let read_task = async move {
+ loop {
+ let len = match reader.read_u32().await {
+ Ok(len) => len,
+ Err(e) => {
+ log_read_end(&e);
+ break;
+ }
+ };
+ let mut buf = BytesMut::zeroed(len as usize);
+ if let Err(e) = reader.read_exact(&mut buf).await {
+ log_read_end(&e);
+ break;
+ }
+ if tx.send(buf.freeze()).is_err() {
+ break;
+ }
+ }
+ };
+ let write_task = async move {
+ while let Some(msg) = rx.recv().await {
+ if let Err(e) = write_frame(&mut writer, msg).await {
+ log_write_end(&e);
+ break;
+ }
+ }
+ };
+ select! {
+ _ = read_task => {},
+ _ = write_task => {},
+ }
+ })
+}
+
+fn log_read_end(e: &io::Error) {
+ if matches!(
+ e.kind(),
+ io::ErrorKind::UnexpectedEof
+ | io::ErrorKind::BrokenPipe
+ | io::ErrorKind::ConnectionReset
+ | io::ErrorKind::ConnectionAborted
+ ) {
+ tracing::debug!("child read ended: {e}");
+ } else {
+ tracing::error!("child read failed: {e}");
+ }
+}
+
+fn log_write_end(e: &io::Error) {
+ if matches!(
+ e.kind(),
+ io::ErrorKind::BrokenPipe
+ | io::ErrorKind::ConnectionReset
+ | io::ErrorKind::ConnectionAborted
+ ) {
+ tracing::debug!("child write ended: {e}");
+ } else {
+ tracing::error!("child write failed: {e}");
+ }
+}
+
+async fn write_frame<W: AsyncWrite + Unpin>(writer: &mut W, msg: Bytes) -> io::Result<()> {
+ let len = u32::try_from(msg.len())
+ .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "message larger than 4GB"))?;
+ writer.write_u32(len).await?;
+ writer.write_all(&msg).await?;
+ writer.flush().await?;
+ Ok(())
+}
remowt/crates/remowt-plugin/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-plugin/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "remowt-plugin"
+description = "Plugin host and protocol for remowt agents"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+bifrostlink.workspace = true
+bifrostlink-ports.workspace = true
+remowt-link-shared.workspace = true
+serde_json.workspace = true
+tokio = { workspace = true, features = [
+ "rt",
+ "net",
+ "io-std",
+ "io-util",
+ "macros",
+ "time",
+ "process",
+] }
+tracing-subscriber.workspace = true
remowt/crates/remowt-plugin/src/host.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-plugin/src/host.rs
@@ -0,0 +1,70 @@
+use std::ffi::OsStr;
+use std::process::Stdio;
+use std::sync::Mutex;
+
+use bifrostlink::{Rpc, Rtt, WeakRpc};
+use tokio::process::{Child, Command};
+
+use remowt_link_shared::plugin::{Error, PluginEndpoints, PluginHost};
+use remowt_link_shared::port::child_port;
+use remowt_link_shared::{Address, BifConfig};
+
+pub fn serve(rpc: &mut Rpc<BifConfig>) {
+ let host = Host {
+ me: rpc.me(),
+ rpc: rpc.clone().downgrade(),
+ children: Mutex::new(Vec::new()),
+ };
+ PluginEndpoints(host).register_endpoints(rpc);
+}
+
+struct Host {
+ me: Address,
+ rpc: WeakRpc<BifConfig>,
+ children: Mutex<Vec<Child>>,
+}
+
+impl Host {
+ fn spawn(&self, id: u16, path: impl AsRef<OsStr>) -> Result<(), Error> {
+ let rpc = self.rpc.clone().upgrade().ok_or(Error::Gone)?;
+
+ let mut child = Command::new(path)
+ .arg(id.to_string())
+ .arg(serde_json::to_string(&self.me).expect("address serializes"))
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .kill_on_drop(true)
+ .spawn()
+ .map_err(|e| Error::Spawn(e.to_string()))?;
+ let stdin = child.stdin.take().expect("stdin piped");
+ let stdout = child.stdout.take().expect("stdout piped");
+
+ let addr = Address::Plugin(id);
+ rpc.add_direct(addr, child_port(stdout, stdin), Rtt(0));
+ self.children.lock().expect("not poisoned").push(child);
+ Ok(())
+ }
+}
+
+impl PluginHost for Host {
+ async fn load_plugin(&self, id: u16, name: String) -> Result<(), Error> {
+ // TODO: Right now loads plugin next to the binary...
+ // But with our CA addressed schema, the plugins should be located in content-addressed subdir...
+ // Maybe it should just be scrapped in favor of load_plugin_path.
+ if name.is_empty() || name == "." || name == ".." || name.contains(['/', '\0']) {
+ return Err(Error::BadName);
+ }
+ let exe = std::env::current_exe().map_err(|e| Error::Spawn(e.to_string()))?;
+ let dir = exe
+ .parent()
+ .ok_or_else(|| Error::Spawn("primary agent has no parent directory".to_owned()))?;
+ self.spawn(id, dir.join(&name))
+ }
+
+ async fn load_plugin_path(&self, id: u16, path: String) -> Result<(), Error> {
+ if path.is_empty() || path.contains('\0') {
+ return Err(Error::BadName);
+ }
+ self.spawn(id, path)
+ }
+}
remowt/crates/remowt-plugin/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-plugin/src/lib.rs
@@ -0,0 +1,46 @@
+use std::future::pending;
+
+use anyhow::Result;
+use bifrostlink::{Rpc, Rtt};
+use bifrostlink_ports::stdio::from_stdio;
+use tokio::runtime::Builder;
+
+pub mod host;
+
+pub use bifrostlink;
+pub use remowt_link_shared::{self, Address, BifConfig};
+
+pub fn plugin_index() -> Result<u16> {
+ let arg = std::env::args()
+ .nth(1)
+ .ok_or_else(|| anyhow::anyhow!("missing plugin index argument"))?;
+ arg.parse()
+ .map_err(|e| anyhow::anyhow!("invalid plugin index {arg:?}: {e}"))
+}
+
+pub fn host_address() -> Result<Address> {
+ let arg = std::env::args()
+ .nth(2)
+ .ok_or_else(|| anyhow::anyhow!("missing host address argument"))?;
+ serde_json::from_str(&arg).map_err(|e| anyhow::anyhow!("invalid host address {arg:?}: {e}"))
+}
+
+pub fn run<F>(register: F) -> Result<()>
+where
+ F: FnOnce(&mut Rpc<BifConfig>),
+{
+ tracing_subscriber::fmt()
+ .with_writer(std::io::stderr)
+ .init();
+
+ let index = plugin_index()?;
+ let host = host_address()?;
+ let runtime = Builder::new_current_thread().enable_all().build()?;
+ runtime.block_on(async move {
+ let mut rpc = Rpc::<BifConfig>::new(Address::Plugin(index));
+ rpc.add_direct(host, from_stdio(), Rtt(0));
+ register(&mut rpc);
+ let _rpc = rpc;
+ pending::<Result<()>>().await
+ })
+}
remowt/crates/remowt-ui-prompt/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-ui-prompt/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "remowt-ui-prompt"
+description = "Interactive UI prompt endpoint for remowt (D-Bus)"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+bifrostlink.workspace = true
+bifrostlink-macros.workspace = true
+serde.workspace = true
+thiserror.workspace = true
+tokio = { workspace = true, features = ["io-util", "macros", "process", "rt"] }
+tracing.workspace = true
+zbus = { workspace = true, optional = true }
+
+[features]
+default = ["dbus"]
+dbus = ["dep:zbus"]
remowt/crates/remowt-ui-prompt/src/auto.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-ui-prompt/src/auto.rs
@@ -0,0 +1,83 @@
+use anyhow::bail;
+use tracing::debug;
+use zbus::fdo::DBusProxy;
+use zbus::names::BusName;
+
+use crate::dbus::{DbusPrompterProxy, BUS_NAME, PROMPTER_PATH};
+use crate::rofi::RofiPrompter;
+use crate::{Prompter, Result, Source};
+
+pub struct AutoPrompter {
+ dbus: Option<DbusPrompterProxy<'static>>,
+ fallback: RofiPrompter,
+}
+
+impl AutoPrompter {
+ pub async fn new() -> Self {
+ let dbus = match Self::try_dbus().await {
+ Ok(p) => Some(p),
+ Err(e) => {
+ debug!("dbus prompter unavailable, falling back to rofi: {e}");
+ None
+ }
+ };
+ Self {
+ dbus,
+ fallback: RofiPrompter,
+ }
+ }
+
+ async fn try_dbus() -> anyhow::Result<DbusPrompterProxy<'static>> {
+ let conn = zbus::Connection::session().await?;
+ let dbus = DBusProxy::new(&conn).await?;
+ let name = BusName::try_from(BUS_NAME)?;
+ if !dbus.name_has_owner(name).await? {
+ bail!("{BUS_NAME} not registered on session bus");
+ }
+ let proxy = DbusPrompterProxy::builder(&conn)
+ .destination(BUS_NAME)?
+ .path(PROMPTER_PATH)?
+ .build()
+ .await?;
+ Ok(proxy)
+ }
+}
+
+impl Prompter for AutoPrompter {
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ if let Some(dbus) = &self.dbus {
+ return Prompter::prompt_enum(dbus, prompt, description, variants, source).await;
+ }
+ self.fallback
+ .prompt_enum(prompt, description, variants, source)
+ .await
+ }
+
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ if let Some(dbus) = &self.dbus {
+ return Prompter::prompt_text(dbus, echo, prompt, description, source).await;
+ }
+ self.fallback
+ .prompt_text(echo, prompt, description, source)
+ .await
+ }
+
+ async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ if let Some(dbus) = &self.dbus {
+ return Prompter::display_text(dbus, error, description, source).await;
+ }
+ self.fallback.display_text(error, description, source).await
+ }
+}
remowt/crates/remowt-ui-prompt/src/bifrost.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-ui-prompt/src/bifrost.rs
@@ -0,0 +1,109 @@
+use bifrostlink::{Config, Rpc};
+use bifrostlink_macros::endpoints;
+use serde::{Deserialize, Serialize};
+
+use crate::{Error, Prompter, Source};
+
+pub struct PromptEndpoints<P>(pub P);
+
+#[endpoints(ns = 2)]
+impl<P> PromptEndpoints<P>
+where
+ P: Prompter + Send + Sync + 'static,
+{
+ #[endpoints(id = 1, cancel)]
+ async fn prompt_enum(
+ &self,
+ prompt: String,
+ description: String,
+ variants: Vec<String>,
+ source: Vec<Source>,
+ ) -> Result<u32, Error> {
+ let variants: Vec<&str> = variants.iter().map(|v| v.as_str()).collect();
+ self.0
+ .prompt_enum(&prompt, &description, &variants, &source)
+ .await
+ }
+
+ #[endpoints(id = 2, cancel)]
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: String,
+ description: String,
+ source: Vec<Source>,
+ ) -> Result<String, Error> {
+ self.0
+ .prompt_text(echo, &prompt, &description, &source)
+ .await
+ }
+
+ #[endpoints(id = 3, cancel)]
+ async fn display_text(
+ &self,
+ error: bool,
+ description: String,
+ source: Vec<Source>,
+ ) -> Result<(), Error> {
+ self.0.display_text(error, &description, &source).await
+ }
+}
+
+impl<C: Config> Prompter for PromptEndpointsClient<C>
+where
+ Error: ToString,
+{
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> crate::Result<u32> {
+ self.prompt_enum(
+ prompt.to_owned(),
+ description.to_owned(),
+ variants.iter().map(|v| (*v).to_owned()).collect(),
+ source.to_vec(),
+ )
+ .await
+ .map_err(|e| Error::Remote(e.to_string()))?
+ }
+
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> crate::Result<String> {
+ self.prompt_text(
+ echo,
+ prompt.to_owned(),
+ description.to_owned(),
+ source.to_vec(),
+ )
+ .await
+ .map_err(|e| Error::Remote(e.to_string()))?
+ }
+
+ async fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: &[Source],
+ ) -> crate::Result<()> {
+ self.display_text(error, description.to_owned(), source.to_vec())
+ .await
+ .map_err(|e| Error::Remote(e.to_string()))?
+ }
+}
+
+pub fn serve_prompts<P, C>(rpc: &mut Rpc<C>, prompt: P)
+where
+ P: Prompter + Send + Sync + 'static,
+ C: Config,
+ C::Error: From<Error>,
+{
+ PromptEndpoints(prompt).register_endpoints(rpc);
+}
remowt/crates/remowt-ui-prompt/src/dbus.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-ui-prompt/src/dbus.rs
@@ -0,0 +1,143 @@
+use zbus::interface;
+use zbus::{fdo, proxy};
+
+use crate::Source;
+use crate::{BlockingPrompter, Result};
+use crate::{Error, Prompter};
+
+pub const BUS_NAME: &str = "lach.RemowtAskpass";
+pub const PROMPTER_PATH: &str = "/lach/Askpass";
+
+pub struct DbusPrompterInterface<P>(pub P);
+
+#[interface(name = "lach.PolkitInputHandler")]
+impl<P: Prompter + Send + Sync + 'static> DbusPrompterInterface<P> {
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: Vec<String>,
+ source: Vec<Source>,
+ ) -> fdo::Result<u32> {
+ let variants: Vec<&str> = variants.iter().map(|v| v.as_str()).collect();
+ Ok(self
+ .0
+ .prompt_enum(prompt, description, &variants, &source)
+ .await?)
+ }
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: Vec<Source>,
+ ) -> fdo::Result<String> {
+ Ok(self
+ .0
+ .prompt_text(echo, prompt, description, &source)
+ .await?)
+ }
+ async fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: Vec<Source>,
+ ) -> fdo::Result<()> {
+ Ok(self.0.display_text(error, description, &source).await?)
+ }
+}
+
+#[proxy(interface = "lach.PolkitInputHandler")]
+pub trait DbusPrompter {
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> fdo::Result<u32>;
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> fdo::Result<String>;
+ async fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: &[Source],
+ ) -> fdo::Result<()>;
+}
+
+impl Prompter for DbusPrompterProxy<'_> {
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ Ok(self
+ .prompt_enum(prompt, description, variants, source)
+ .await?)
+ }
+
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ Ok(self.prompt_text(echo, prompt, description, source).await?)
+ }
+
+ async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ Ok(self.display_text(error, description, source).await?)
+ }
+}
+impl BlockingPrompter for DbusPrompterProxyBlocking<'_> {
+ fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ Ok(self.prompt_enum(prompt, description, variants, source)?)
+ }
+
+ fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ Ok(self.prompt_text(echo, prompt, description, source)?)
+ }
+
+ fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ Ok(self.display_text(error, description, source)?)
+ }
+}
+
+impl From<fdo::Error> for Error {
+ fn from(value: fdo::Error) -> Self {
+ if matches!(value, fdo::Error::NoReply(_)) {
+ return Self::Cancel;
+ }
+ Self::InputError(format!("{value}"))
+ }
+}
+impl From<Error> for fdo::Error {
+ fn from(value: Error) -> Self {
+ match value {
+ Error::Cancel => fdo::Error::NoReply("input was cancelled".to_owned()),
+ Error::Remote(e) => fdo::Error::NoReply(format!("remote error occured: {e}")),
+ Error::InputError(e) => fdo::Error::Failed(e),
+ }
+ }
+}
remowt/crates/remowt-ui-prompt/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-ui-prompt/src/lib.rs
@@ -0,0 +1,202 @@
+use core::fmt;
+use std::borrow::Cow;
+use std::future::Future;
+use std::result;
+
+pub mod auto;
+pub mod bifrost;
+pub mod dbus;
+pub mod rofi;
+
+#[derive(thiserror::Error, Debug, serde::Serialize, serde::Deserialize)]
+pub enum Error {
+ #[error("user has cancelled input")]
+ Cancel,
+ #[error("input error: {0}")]
+ InputError(String),
+ #[error("unknown remote error: {0}")]
+ Remote(String),
+}
+
+pub type Result<T, E = Error> = result::Result<T, E>;
+
+#[cfg_attr(feature = "dbus", derive(zbus::zvariant::Type))]
+#[derive(serde::Serialize, serde::Deserialize, Clone)]
+pub struct Source(pub Cow<'static, str>);
+impl fmt::Display for Source {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "<u>{}</u>", self.0)
+ }
+}
+
+pub trait Prompter: Send + Sync {
+ fn prompt_radio(
+ &self,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<bool>> + Send {
+ let fut = self.prompt_enum(prompt, description, &["No", "Yes"], source);
+ async { fut.await.map(|v| v == 1) }
+ }
+ fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> impl Future<Output = Result<u32>> + Send;
+ fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<String>> + Send;
+ fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<()>> + Send;
+}
+pub trait BlockingPrompter {
+ fn prompt_radio(&self, prompt: &str, description: &str, source: &[Source]) -> Result<bool> {
+ self.prompt_enum(prompt, description, &["No", "Yes"], source)
+ .map(|v| v == 1)
+ }
+ fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32>;
+ fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String>;
+ fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()>;
+}
+impl<P> Prompter for &P
+where
+ P: Prompter,
+{
+ fn prompt_radio(
+ &self,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<bool>> + Send {
+ (*self).prompt_radio(prompt, description, source)
+ }
+
+ fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> impl Future<Output = Result<u32>> + Send {
+ (*self).prompt_enum(prompt, description, variants, source)
+ }
+
+ fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<String>> + Send {
+ (*self).prompt_text(echo, prompt, description, source)
+ }
+
+ fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<()>> + Send {
+ (*self).display_text(error, description, source)
+ }
+}
+
+pub struct PrependSourcePrompter<P> {
+ pub prompter: P,
+ pub source: Vec<Source>,
+ pub description: String,
+}
+impl<P> PrependSourcePrompter<P> {
+ fn source(&self, input: &[Source]) -> Vec<Source> {
+ let mut out = self.source.clone();
+ out.extend(input.iter().cloned());
+ out
+ }
+ fn description(&self, input: &str) -> String {
+ if self.description.is_empty() {
+ input.to_owned()
+ } else if input.is_empty() {
+ self.description.to_owned()
+ } else {
+ format!("{input}\n\n{}", self.description)
+ }
+ }
+}
+impl<P> Prompter for PrependSourcePrompter<P>
+where
+ P: Prompter + Sync,
+{
+ async fn prompt_radio(
+ &self,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<bool> {
+ self.prompter
+ .prompt_radio(prompt, &self.description(description), &self.source(source))
+ .await
+ }
+
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ self.prompter
+ .prompt_enum(
+ prompt,
+ &self.description(description),
+ variants,
+ &self.source(source),
+ )
+ .await
+ }
+
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ self.prompter
+ .prompt_text(
+ echo,
+ prompt,
+ &self.description(description),
+ &self.source(source),
+ )
+ .await
+ }
+
+ async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ self.prompter
+ .display_text(error, &self.description(description), &self.source(source))
+ .await
+ }
+}
remowt/crates/remowt-ui-prompt/src/rofi.rsdiffbeforeafterboth--- /dev/null
+++ b/remowt/crates/remowt-ui-prompt/src/rofi.rs
@@ -0,0 +1,226 @@
+use std::process::Stdio;
+
+use tokio::io::AsyncWriteExt;
+use tokio::process::Command;
+use tracing::trace;
+
+use crate::{Error, Prompter, Result, Source};
+
+#[derive(Clone)]
+pub struct RofiPrompter;
+
+fn fixup_prompt(prompt: &str) -> &str {
+ // Rofi always appends such suffix
+ prompt.strip_suffix(": ").unwrap_or(prompt)
+}
+
+fn rofi_command() -> Command {
+ Command::new(option_env!("ROFI").unwrap_or("rofi"))
+}
+
+impl Prompter for RofiPrompter {
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ trace!("rofi radio");
+ let mut cmd = rofi_command();
+ let mesg = if source.is_empty() {
+ description.to_owned()
+ } else {
+ let mut out = format!("{description}\n\n<b>Requested on ",);
+ for (i, s) in source.iter().enumerate() {
+ if i != 0 {
+ out.push_str(" -> ");
+ }
+ out.push_str(&s.to_string());
+ }
+ out.push_str("</b>");
+ out
+ };
+ cmd.args([
+ "-dmenu",
+ "-mesg",
+ &mesg,
+ "-sync",
+ "-no-custom",
+ "-p",
+ fixup_prompt(prompt),
+ "-format",
+ "i",
+ "-markup-rows",
+ ]);
+ cmd.stdin(Stdio::piped());
+ cmd.stdout(Stdio::piped());
+ cmd.kill_on_drop(true);
+ let mut child = cmd
+ .spawn()
+ .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
+
+ let mut stdin = child.stdin.take().expect("stdin is piped");
+ for var in variants {
+ stdin
+ .write_all(var.replace('\n', " ").as_bytes())
+ .await
+ .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;
+ stdin
+ .write_all(b"\n")
+ .await
+ .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;
+ }
+ // write_all already flushes, just to be sure.
+ let _ = stdin.flush().await;
+ drop(stdin);
+
+ let out = child
+ .wait_with_output()
+ .await
+ .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
+ match out.status.code() {
+ Some(0) => {}
+ Some(1) => return Err(Error::Cancel),
+ other => {
+ return Err(Error::InputError(format!(
+ "rofi exited with status {other:?}"
+ )));
+ }
+ }
+ let stdout = out
+ .stdout
+ .strip_suffix(b"\n")
+ .unwrap_or(&out.stdout)
+ .to_owned();
+
+ let id: u32 = String::from_utf8(stdout)
+ .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?
+ .parse()
+ .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?;
+ if id as usize >= variants.len() {
+ return Err(Error::InputError("invalid rofi response".to_owned()));
+ }
+
+ Ok(id)
+ }
+
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ trace!("rofi text");
+ let mut cmd = rofi_command();
+ let mesg = if source.is_empty() {
+ description.to_owned()
+ } else {
+ let mut out = format!("{description}\n\n<b>Requested on ",);
+ for (i, s) in source.iter().enumerate() {
+ if i != 0 {
+ out.push_str(" -> ");
+ }
+ out.push_str(&s.to_string());
+ }
+ out.push_str("</b>");
+ out
+ };
+ cmd.args(["-dmenu", "-mesg", &mesg, "-p", fixup_prompt(prompt)]);
+ if !echo {
+ cmd.arg("-password");
+ }
+ cmd.stdin(Stdio::null());
+ cmd.stdout(Stdio::piped());
+ cmd.kill_on_drop(true);
+ let child = cmd
+ .spawn()
+ .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
+
+ let out = child
+ .wait_with_output()
+ .await
+ .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
+ match out.status.code() {
+ Some(0) => {}
+ Some(1) => return Err(Error::Cancel),
+ other => {
+ return Err(Error::InputError(format!(
+ "rofi exited with status {other:?}"
+ )));
+ }
+ }
+ let stdout = out
+ .stdout
+ .strip_suffix(b"\n")
+ .unwrap_or(&out.stdout)
+ .to_owned();
+
+ Ok(String::from_utf8_lossy(&stdout).to_string())
+ }
+
+ async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ trace!("rofi display");
+ let mut cmd = rofi_command();
+ let mut mesg = if source.is_empty() {
+ description.to_owned()
+ } else {
+ let mut out = format!("{description}\n\n<b>Coming from ",);
+ for s in source.iter() {
+ out.push_str(&s.to_string());
+ }
+ out.push_str("</b>");
+ out
+ };
+ if error {
+ mesg.insert_str(0, "<span color=\"red\">");
+ mesg.push_str("</span>");
+ }
+ cmd.args(["-e", &mesg, "-markup"]);
+ cmd.stdin(Stdio::null());
+ cmd.stdout(Stdio::null());
+ cmd.kill_on_drop(true);
+ let mut child = cmd
+ .spawn()
+ .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
+
+ child
+ .wait()
+ .await
+ .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
+
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::borrow::Cow;
+
+ use crate::rofi::RofiPrompter;
+ use crate::{PrependSourcePrompter, Prompter as _, Source};
+
+ // #[tokio::test]
+ #[tokio::test]
+ #[ignore = "interactive"]
+ async fn test() {
+ let prompter = PrependSourcePrompter {
+ prompter: RofiPrompter,
+ description: "test".to_owned(),
+ source: vec![Source(Cow::Borrowed("ssh"))],
+ };
+ prompter
+ .prompt_radio("Enable", "Polkit needs access", &[])
+ .await
+ .expect("rofi");
+ prompter
+ .prompt_text(false, "Password", "Polkit needs access", &[])
+ .await
+ .expect("rofi");
+ prompter
+ .display_text(true, "Polkit needs access", &[])
+ .await
+ .expect("rofi");
+ }
+}