git.delta.rocks / fleet / refs/commits / 7bc4be6bcef6

difftreelog

feat remowt integration

kyumtlkpYaroslav Bolyukin2026-06-05parent: #7bbada6.patch.diff

46 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,15 +3,6 @@
 version = 4
 
 [[package]]
-name = "abort-on-drop"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dd6d700ad9af641490c1f7d67980d2de4d1433016e5b12f819448d3c832142a"
-dependencies = [
- "tokio",
-]
-
-[[package]]
 name = "adler2"
 version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -23,54 +14,91 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
 dependencies = [
- "crypto-common",
- "generic-array",
+ "crypto-common 0.1.7",
+ "generic-array 0.14.7",
 ]
 
 [[package]]
+name = "aead"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef60ac202874e574ce7a7158cc8bca7313dd344322482e4fadee288bf4a306b8"
+dependencies = [
+ "crypto-common 0.2.2",
+ "inout 0.2.2",
+]
+
+[[package]]
 name = "aes"
 version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
 dependencies = [
  "cfg-if",
- "cipher",
+ "cipher 0.4.4",
  "cpufeatures 0.2.17",
 ]
 
 [[package]]
+name = "aes"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138"
+dependencies = [
+ "cipher 0.5.2",
+ "cpubits",
+ "cpufeatures 0.3.0",
+ "zeroize",
+]
+
+[[package]]
 name = "aes-gcm"
 version = "0.10.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
 dependencies = [
- "aead",
- "aes",
- "cipher",
- "ctr",
- "ghash",
+ "aead 0.5.2",
+ "aes 0.8.4",
+ "cipher 0.4.4",
+ "ctr 0.9.2",
+ "ghash 0.5.1",
  "subtle",
 ]
 
 [[package]]
+name = "aes-gcm"
+version = "0.11.0-rc.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da8c919c118108f144adecad74b425b804ad075580d605d9b33c2d6d1c62a2f8"
+dependencies = [
+ "aead 0.6.0",
+ "aes 0.9.1",
+ "cipher 0.5.2",
+ "ctr 0.10.1",
+ "ghash 0.6.0",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
 name = "age"
-version = "0.11.2"
+version = "0.11.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf640be7658959746f1f0f2faab798f6098a9436a8e18e148d18bc9875e13c4b"
+checksum = "a07d86e4272c093c88caf7864a2d09af52a5159180848ca4832a3cdbd7d014d5"
 dependencies = [
- "aes",
- "aes-gcm",
+ "aes 0.8.4",
+ "aes-gcm 0.10.3",
  "age-core",
  "base64 0.21.7",
- "bcrypt-pbkdf",
+ "bcrypt-pbkdf 0.10.0",
  "bech32",
- "cbc",
+ "cbc 0.1.2",
  "chacha20poly1305",
- "cipher",
+ "cipher 0.4.4",
  "cookie-factory",
- "ctr",
- "curve25519-dalek",
- "hmac",
+ "ctr 0.9.2",
+ "curve25519-dalek 4.1.3",
+ "hmac 0.12.1",
  "i18n-embed",
  "i18n-embed-fl",
  "lazy_static",
@@ -78,10 +106,10 @@
  "num-traits",
  "pin-project",
  "rand 0.8.6",
- "rsa",
+ "rsa 0.9.10",
  "rust-embed",
- "scrypt",
- "sha2",
+ "scrypt 0.11.0",
+ "sha2 0.10.9",
  "subtle",
  "which",
  "wsl",
@@ -98,12 +126,12 @@
  "base64 0.21.7",
  "chacha20poly1305",
  "cookie-factory",
- "hkdf",
+ "hkdf 0.12.4",
  "io_tee",
  "nom 7.1.3",
  "rand 0.8.6",
  "secrecy",
- "sha2",
+ "sha2 0.10.9",
  "tempfile",
 ]
 
@@ -191,34 +219,144 @@
 ]
 
 [[package]]
+name = "argon2"
+version = "0.6.0-rc.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7af50940b73bf4e16c15c448a2b121c63f2d68e3e54b6a8731673cb4aa0cdff5"
+dependencies = [
+ "base64ct",
+ "blake2",
+ "cpufeatures 0.3.0",
+ "password-hash",
+]
+
+[[package]]
 name = "arrayvec"
 version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
 
 [[package]]
-name = "async-stream"
-version = "0.3.6"
+name = "async-broadcast"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
 dependencies = [
- "async-stream-impl",
+ "concurrent-queue",
+ "event-listener-strategy",
  "futures-core",
  "pin-project-lite",
 ]
 
 [[package]]
-name = "async-stream-impl"
-version = "0.3.6"
+name = "async-executor"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix 1.1.4",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix 1.1.4",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix 1.1.4",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
 name = "async-trait"
 version = "0.1.89"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -226,10 +364,19 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
+name = "async_fn_traits"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc58d489c5f2d2c5be31b9004cec7af25a70d23df4d8111715eee736234cf217"
+dependencies = [
+ "paste",
+]
+
+[[package]]
 name = "atomic-waker"
 version = "1.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -237,56 +384,15 @@
 
 [[package]]
 name = "autocfg"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
-
-[[package]]
-name = "axum"
-version = "0.7.9"
+version = "1.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
-dependencies = [
- "async-trait",
- "axum-core",
- "bytes",
- "futures-util",
- "http",
- "http-body",
- "http-body-util",
- "itoa",
- "matchit",
- "memchr",
- "mime",
- "percent-encoding",
- "pin-project-lite",
- "rustversion",
- "serde",
- "sync_wrapper",
- "tower 0.5.3",
- "tower-layer",
- "tower-service",
-]
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
 
 [[package]]
-name = "axum-core"
-version = "0.4.5"
+name = "base16ct"
+version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
-dependencies = [
- "async-trait",
- "bytes",
- "futures-util",
- "http",
- "http-body",
- "http-body-util",
- "mime",
- "pin-project-lite",
- "rustversion",
- "sync_wrapper",
- "tower-layer",
- "tower-service",
-]
+checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6"
 
 [[package]]
 name = "base64"
@@ -321,9 +427,20 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2"
 dependencies = [
- "blowfish",
- "pbkdf2",
- "sha2",
+ "blowfish 0.9.1",
+ "pbkdf2 0.12.2",
+ "sha2 0.10.9",
+]
+
+[[package]]
+name = "bcrypt-pbkdf"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "144e573728da132683b9488acd528274c790e07fc06ff81ee29f9d8f8b1041e0"
+dependencies = [
+ "blowfish 0.10.0",
+ "pbkdf2 0.13.0",
+ "sha2 0.11.0",
 ]
 
 [[package]]
@@ -333,14 +450,45 @@
 checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445"
 
 [[package]]
-name = "better-command"
-version = "0.1.0"
+name = "bifrostlink"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fb01af731c11dd31b23783a83a36a29f644cc1972481f6fa4f4fabc709079eb"
 dependencies = [
- "regex",
+ "async-trait",
+ "async_fn_traits",
+ "bifrostlink-macros",
+ "bytes",
+ "derivative",
+ "futures",
  "serde",
  "serde_json",
+ "tokio",
+ "tracing",
+ "uuid",
+]
+
+[[package]]
+name = "bifrostlink-macros"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c4b7a5fb38b36bd81910c17ebf369f9296e508d92b1277a768a63c8a2254fdb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "bifrostlink-ports"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "977acfcb8ed3c24ab7c2f76fb3eeebff1533c72708733ce6020f2501980b7cf2"
+dependencies = [
+ "bifrostlink",
+ "bytes",
+ "tokio",
  "tracing",
- "tracing-indicatif",
 ]
 
 [[package]]
@@ -359,15 +507,24 @@
  "quote",
  "regex",
  "rustc-hash 2.1.2",
- "shlex",
- "syn",
+ "shlex 1.3.0",
+ "syn 2.0.117",
 ]
 
 [[package]]
 name = "bitflags"
-version = "2.11.1"
+version = "2.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
+
+[[package]]
+name = "blake2"
+version = "0.11.0-rc.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "061f1a09225e328e1ffbb378d2d49923c0ca5fee19fb5ac1cc9c1e9d52b93690"
+dependencies = [
+ "digest 0.11.3",
+]
 
 [[package]]
 name = "block-buffer"
@@ -375,33 +532,85 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
 dependencies = [
- "generic-array",
+ "generic-array 0.14.7",
 ]
 
 [[package]]
+name = "block-buffer"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
+dependencies = [
+ "hybrid-array",
+ "zeroize",
+]
+
+[[package]]
 name = "block-padding"
 version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
 dependencies = [
- "generic-array",
+ "generic-array 0.14.7",
 ]
 
 [[package]]
+name = "block-padding"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "blocking"
+version = "1.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
+[[package]]
 name = "blowfish"
 version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
 dependencies = [
  "byteorder",
- "cipher",
+ "cipher 0.4.4",
+]
+
+[[package]]
+name = "blowfish"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62ce3946557b35e71d1bbe07ec385073ce9eda05043f95de134eb578fcf1a298"
+dependencies = [
+ "byteorder",
+ "cipher 0.5.2",
+]
+
+[[package]]
+name = "bstr"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
+dependencies = [
+ "memchr",
+ "serde",
 ]
 
 [[package]]
 name = "bumpalo"
-version = "3.20.2"
+version = "3.20.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
 
 [[package]]
 name = "bytecount"
@@ -422,22 +631,40 @@
 checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
 
 [[package]]
+name = "camino"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
 name = "cbc"
 version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
 dependencies = [
- "cipher",
+ "cipher 0.4.4",
 ]
 
 [[package]]
+name = "cbc"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896"
+dependencies = [
+ "cipher 0.5.2",
+]
+
+[[package]]
 name = "cc"
-version = "1.2.60"
+version = "1.2.64"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
+checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f"
 dependencies = [
  "find-msvc-tools",
- "shlex",
+ "shlex 2.0.1",
 ]
 
 [[package]]
@@ -468,7 +695,7 @@
 checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
 dependencies = [
  "cfg-if",
- "cipher",
+ "cipher 0.4.4",
  "cpufeatures 0.2.17",
 ]
 
@@ -479,8 +706,10 @@
 checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
 dependencies = [
  "cfg-if",
+ "cipher 0.5.2",
  "cpufeatures 0.3.0",
  "rand_core 0.10.1",
+ "zeroize",
 ]
 
 [[package]]
@@ -489,18 +718,18 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
 dependencies = [
- "aead",
+ "aead 0.5.2",
  "chacha20 0.9.1",
- "cipher",
- "poly1305",
+ "cipher 0.4.4",
+ "poly1305 0.8.0",
  "zeroize",
 ]
 
 [[package]]
 name = "chrono"
-version = "0.4.44"
+version = "0.4.45"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
+checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
 dependencies = [
  "iana-time-zone",
  "js-sys",
@@ -516,8 +745,20 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
 dependencies = [
- "crypto-common",
- "inout",
+ "crypto-common 0.1.7",
+ "inout 0.1.4",
+ "zeroize",
+]
+
+[[package]]
+name = "cipher"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c"
+dependencies = [
+ "block-buffer 0.12.1",
+ "crypto-common 0.2.2",
+ "inout 0.2.2",
  "zeroize",
 ]
 
@@ -559,9 +800,9 @@
 
 [[package]]
 name = "clap_complete"
-version = "4.6.2"
+version = "4.6.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ff7a1dccbdd8b078c2bdebff47e404615151534d5043da397ec50286816f9cb"
+checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772"
 dependencies = [
  "clap",
 ]
@@ -575,7 +816,7 @@
  "heck",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -585,6 +826,12 @@
 checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
 
 [[package]]
+name = "cmov"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
+
+[[package]]
 name = "codespan-reporting"
 version = "0.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -602,6 +849,15 @@
 checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
 
 [[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
 name = "console"
 version = "0.16.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -615,9 +871,9 @@
 
 [[package]]
 name = "const-hex"
-version = "1.18.1"
+version = "1.19.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "531185e432bb31db1ecda541e9e7ab21468d4d844ad7505e0546a49b4945d49b"
+checksum = "33e2a781ebdf4467d1428dc4593067825fb646f6871475098d8577421af73558"
 dependencies = [
  "cfg-if",
  "cpufeatures 0.2.17",
@@ -632,6 +888,12 @@
 checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
 
 [[package]]
+name = "const-oid"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
+
+[[package]]
 name = "cookie-factory"
 version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -657,6 +919,12 @@
 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
 
 [[package]]
+name = "cpubits"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae"
+
+[[package]]
 name = "cpufeatures"
 version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -684,23 +952,86 @@
 ]
 
 [[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto-bigint"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42a0d26b245348befa0c121944541476763dcc46ede886c88f9d12e1697d27c3"
+dependencies = [
+ "cpubits",
+ "ctutils",
+ "getrandom 0.4.2",
+ "hybrid-array",
+ "num-traits",
+ "rand_core 0.10.1",
+ "serdect",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
 name = "crypto-common"
 version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
 dependencies = [
- "generic-array",
+ "generic-array 0.14.7",
  "rand_core 0.6.4",
  "typenum",
 ]
 
 [[package]]
+name = "crypto-common"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
+dependencies = [
+ "getrandom 0.4.2",
+ "hybrid-array",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "crypto-primes"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3633a51a39c69ebbaa4feaa694bd83d241e4093901c84a0963b19d9bb3f0cf8f"
+dependencies = [
+ "crypto-bigint",
+ "rand_core 0.10.1",
+]
+
+[[package]]
 name = "ctr"
 version = "0.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
 dependencies = [
- "cipher",
+ "cipher 0.4.4",
+]
+
+[[package]]
+name = "ctr"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21"
+dependencies = [
+ "cipher 0.5.2",
+]
+
+[[package]]
+name = "ctutils"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
+dependencies = [
+ "cmov",
+ "subtle",
 ]
 
 [[package]]
@@ -718,14 +1049,30 @@
  "cfg-if",
  "cpufeatures 0.2.17",
  "curve25519-dalek-derive",
- "digest",
- "fiat-crypto",
+ "digest 0.10.7",
+ "fiat-crypto 0.2.9",
  "rustc_version",
  "subtle",
  "zeroize",
 ]
 
 [[package]]
+name = "curve25519-dalek"
+version = "5.0.0-rc.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f359e08ca85e7bd759e1fd933ff2bccd81864c60a8fba0e259c7f822b0924bf"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "curve25519-dalek-derive",
+ "digest 0.11.3",
+ "fiat-crypto 0.3.0",
+ "rustc_version",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
 name = "curve25519-dalek-derive"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -733,7 +1080,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -759,11 +1106,11 @@
 dependencies = [
  "cc",
  "codespan-reporting",
- "indexmap 2.14.0",
+ "indexmap",
  "proc-macro2",
  "quote",
  "scratch",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -774,10 +1121,10 @@
 dependencies = [
  "clap",
  "codespan-reporting",
- "indexmap 2.14.0",
+ "indexmap",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -792,10 +1139,27 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6acc6b5822b9526adfb4fc377b67128fdd60aac757cc4a741a6278603f763cf"
 dependencies = [
- "indexmap 2.14.0",
+ "indexmap",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
+
+[[package]]
+name = "delegate"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370"
+dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -804,7 +1168,18 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
 dependencies = [
- "const-oid",
+ "const-oid 0.9.6",
+ "zeroize",
+]
+
+[[package]]
+name = "der"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b"
+dependencies = [
+ "const-oid 0.10.2",
+ "pem-rfc7468",
  "zeroize",
 ]
 
@@ -813,9 +1188,25 @@
 version = "0.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+
+[[package]]
+name = "derivative"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "des"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "916a94e407b54f9034d71dd748234cd1e516ced6284009906ae246f177eafe5a"
 dependencies = [
- "powerfmt",
- "serde_core",
+ "cipher 0.5.2",
 ]
 
 [[package]]
@@ -824,21 +1215,48 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
 dependencies = [
- "block-buffer",
- "const-oid",
- "crypto-common",
+ "block-buffer 0.10.4",
+ "const-oid 0.9.6",
+ "crypto-common 0.1.7",
  "subtle",
 ]
 
 [[package]]
+name = "digest"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
+dependencies = [
+ "block-buffer 0.12.1",
+ "const-oid 0.10.2",
+ "crypto-common 0.2.2",
+ "ctutils",
+]
+
+[[package]]
 name = "displaydoc"
-version = "0.2.5"
+version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "ecdsa"
+version = "0.17.0-rc.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54fb064faabbee66e1fc8e5c5a9458d4269dc2d8b638fe86a425adb2510d1a96"
+dependencies = [
+ "der 0.8.0",
+ "digest 0.11.3",
+ "elliptic-curve",
+ "rfc6979",
+ "signature 3.0.0",
+ "spki 0.8.0",
+ "zeroize",
 ]
 
 [[package]]
@@ -847,29 +1265,78 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
 dependencies = [
- "pkcs8",
- "signature",
+ "pkcs8 0.10.2",
+ "signature 2.2.0",
 ]
 
 [[package]]
+name = "ed25519"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a"
+dependencies = [
+ "pkcs8 0.11.0",
+ "signature 3.0.0",
+]
+
+[[package]]
 name = "ed25519-dalek"
 version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
 dependencies = [
- "curve25519-dalek",
- "ed25519",
+ "curve25519-dalek 4.1.3",
+ "ed25519 2.2.3",
  "serde",
- "sha2",
+ "sha2 0.10.9",
  "subtle",
  "zeroize",
 ]
 
 [[package]]
+name = "ed25519-dalek"
+version = "3.0.0-rc.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b011170fe4f04665565b4110afef66774fe9ffff278f3eb5b81cc73d26e27d60"
+dependencies = [
+ "curve25519-dalek 5.0.0-rc.0",
+ "ed25519 3.0.0",
+ "rand_core 0.10.1",
+ "serde",
+ "sha2 0.11.0",
+ "signature 3.0.0",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
 name = "either"
-version = "1.15.0"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+
+[[package]]
+name = "elliptic-curve"
+version = "0.14.0-rc.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+checksum = "102d3643d30dd8b559613c5cced68317199597fffb278cdc88daa2ef7fafc935"
+dependencies = [
+ "base16ct",
+ "crypto-bigint",
+ "crypto-common 0.2.2",
+ "digest 0.11.3",
+ "ff",
+ "group",
+ "hkdf 0.13.0",
+ "hybrid-array",
+ "once_cell",
+ "pem-rfc7468",
+ "pkcs8 0.11.0",
+ "rand_core 0.10.1",
+ "sec1",
+ "subtle",
+ "zeroize",
+]
 
 [[package]]
 name = "encode_unicode"
@@ -878,6 +1345,45 @@
 checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
 
 [[package]]
+name = "endi"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
+
+[[package]]
+name = "enum_dispatch"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd"
+dependencies = [
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
 name = "env_filter"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -915,18 +1421,55 @@
 ]
 
 [[package]]
+name = "event-listener"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
 name = "fastrand"
 version = "2.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
 
 [[package]]
+name = "ff"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f"
+dependencies = [
+ "rand_core 0.10.1",
+ "subtle",
+]
+
+[[package]]
 name = "fiat-crypto"
 version = "0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
 
 [[package]]
+name = "fiat-crypto"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24"
+
+[[package]]
 name = "find-crate"
 version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -940,12 +1483,6 @@
 version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
-
-[[package]]
-name = "fixedbitset"
-version = "0.5.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
 
 [[package]]
 name = "flate2"
@@ -961,42 +1498,27 @@
 name = "fleet"
 version = "0.2.0"
 dependencies = [
- "abort-on-drop",
- "age",
- "age-core",
  "anyhow",
- "async-trait",
- "base64 0.22.1",
- "better-command",
- "chrono",
+ "camino",
  "clap",
  "clap_complete",
  "fleet-base",
- "fleet-shared",
  "futures",
- "hostname",
  "human-repr",
  "indicatif",
  "itertools 0.14.0",
  "nix-eval",
- "nixlike",
- "nom 8.0.0",
- "openssh",
  "opentelemetry",
  "opentelemetry-appender-tracing",
  "opentelemetry-exporter-env",
  "opentelemetry_sdk",
- "peg",
- "regex",
+ "remowt-fleet",
  "serde",
  "serde_json",
- "shlex",
+ "shlex 1.3.0",
  "tabled",
  "tempfile",
- "thiserror 2.0.18",
- "time",
  "tokio",
- "tokio-util",
  "tracing",
  "tracing-indicatif",
  "tracing-opentelemetry",
@@ -1009,7 +1531,7 @@
 dependencies = [
  "age",
  "anyhow",
- "better-command",
+ "camino",
  "chrono",
  "clap",
  "fleet-shared",
@@ -1020,17 +1542,18 @@
  "nix-eval",
  "nixlike",
  "nom 8.0.0",
- "openssh",
  "rand 0.10.1",
+ "remowt-client",
+ "remowt-endpoints",
+ "remowt-fleet",
+ "remowt-link-shared",
+ "remowt-ui-prompt",
  "serde",
  "serde_json",
  "tabled",
  "tempfile",
- "thiserror 2.0.18",
  "time",
  "tokio",
- "tokio-util",
- "toml_edit",
  "tracing",
 ]
 
@@ -1042,13 +1565,13 @@
  "anyhow",
  "base64 0.22.1",
  "clap",
- "ed25519-dalek",
+ "ed25519-dalek 2.2.0",
  "fleet-shared",
  "hex",
- "hmac",
- "pbkdf2",
+ "hmac 0.12.1",
+ "pbkdf2 0.12.2",
  "rand 0.10.1",
- "sha2",
+ "sha2 0.10.9",
  "x25519-dalek",
 ]
 
@@ -1197,6 +1720,19 @@
 checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
 
 [[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
 name = "futures-macro"
 version = "0.3.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1204,7 +1740,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -1247,6 +1783,17 @@
 ]
 
 [[package]]
+name = "generic-array"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2e55f16dcf0e9c00efbe2e655ffe45fc98e7066b52bc92f8a79e64060a79351"
+dependencies = [
+ "generic-array 0.14.7",
+ "rustversion",
+ "typenum",
+]
+
+[[package]]
 name = "getrandom"
 version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1280,11 +1827,13 @@
 checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
 dependencies = [
  "cfg-if",
+ "js-sys",
  "libc",
  "r-efi 6.0.0",
  "rand_core 0.10.1",
  "wasip2",
  "wasip3",
+ "wasm-bindgen",
 ]
 
 [[package]]
@@ -1294,20 +1843,53 @@
 checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
 dependencies = [
  "opaque-debug",
- "polyval",
+ "polyval 0.6.2",
 ]
 
 [[package]]
+name = "ghash"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5"
+dependencies = [
+ "polyval 0.7.1",
+]
+
+[[package]]
 name = "glob"
 version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
 
 [[package]]
+name = "globset"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "log",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "group"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4"
+dependencies = [
+ "ff",
+ "rand_core 0.10.1",
+ "subtle",
+]
+
+[[package]]
 name = "h2"
-version = "0.4.13"
+version = "0.4.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
+checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
 dependencies = [
  "atomic-waker",
  "bytes",
@@ -1315,7 +1897,7 @@
  "futures-core",
  "futures-sink",
  "http",
- "indexmap 2.14.0",
+ "indexmap",
  "slab",
  "tokio",
  "tokio-util",
@@ -1324,12 +1906,6 @@
 
 [[package]]
 name = "hashbrown"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
-
-[[package]]
-name = "hashbrown"
 version = "0.15.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
@@ -1339,9 +1915,9 @@
 
 [[package]]
 name = "hashbrown"
-version = "0.17.0"
+version = "0.17.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
 
 [[package]]
 name = "heck"
@@ -1350,27 +1926,57 @@
 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
 [[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
 name = "hex"
 version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
 
 [[package]]
+name = "hex-literal"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1"
+
+[[package]]
 name = "hkdf"
 version = "0.12.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
 dependencies = [
- "hmac",
+ "hmac 0.12.1",
 ]
 
 [[package]]
+name = "hkdf"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018"
+dependencies = [
+ "hmac 0.13.0",
+]
+
+[[package]]
 name = "hmac"
 version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
 dependencies = [
- "digest",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "hmac"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
+dependencies = [
+ "digest 0.11.3",
 ]
 
 [[package]]
@@ -1395,9 +2001,9 @@
 
 [[package]]
 name = "http"
-version = "1.4.0"
+version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
 dependencies = [
  "bytes",
  "itoa",
@@ -1433,22 +2039,28 @@
 checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
 
 [[package]]
-name = "httpdate"
-version = "1.0.3"
+name = "human-repr"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+checksum = "f58b778a5761513caf593693f8951c97a5b610841e754788400f32102eefdff1"
 
 [[package]]
-name = "human-repr"
-version = "1.1.0"
+name = "hybrid-array"
+version = "0.4.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f58b778a5761513caf593693f8951c97a5b610841e754788400f32102eefdff1"
+checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da"
+dependencies = [
+ "ctutils",
+ "subtle",
+ "typenum",
+ "zeroize",
+]
 
 [[package]]
 name = "hyper"
-version = "1.9.0"
+version = "1.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
+checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
 dependencies = [
  "atomic-waker",
  "bytes",
@@ -1458,7 +2070,6 @@
  "http",
  "http-body",
  "httparse",
- "httpdate",
  "itoa",
  "pin-project-lite",
  "smallvec",
@@ -1512,7 +2123,7 @@
  "libc",
  "percent-encoding",
  "pin-project-lite",
- "socket2 0.6.3",
+ "socket2",
  "tokio",
  "tower-service",
  "tracing",
@@ -1567,7 +2178,7 @@
  "proc-macro2",
  "quote",
  "strsim",
- "syn",
+ "syn 2.0.117",
  "unic-langid",
 ]
 
@@ -1581,7 +2192,7 @@
  "i18n-config",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -1709,9 +2320,9 @@
 
 [[package]]
 name = "idna_adapter"
-version = "1.2.1"
+version = "1.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
 dependencies = [
  "icu_normalizer",
  "icu_properties",
@@ -1719,22 +2330,12 @@
 
 [[package]]
 name = "indexmap"
-version = "1.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
-dependencies = [
- "autocfg",
- "hashbrown 0.12.3",
-]
-
-[[package]]
-name = "indexmap"
 version = "2.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
 dependencies = [
  "equivalent",
- "hashbrown 0.17.0",
+ "hashbrown 0.17.1",
  "serde",
  "serde_core",
 ]
@@ -1768,11 +2369,33 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
 dependencies = [
- "block-padding",
- "generic-array",
+ "block-padding 0.3.3",
+ "generic-array 0.14.7",
 ]
 
 [[package]]
+name = "inout"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7"
+dependencies = [
+ "block-padding 0.4.2",
+ "hybrid-array",
+]
+
+[[package]]
+name = "internal-russh-num-bigint"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae8e22120c32fb4d19ec55fba35015f57095cd95a2e3b732e44457f5915b2ee8"
+dependencies = [
+ "num-integer",
+ "num-traits",
+ "rand 0.10.1",
+ "rand_core 0.10.1",
+]
+
+[[package]]
 name = "intl-memoizer"
 version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1802,16 +2425,6 @@
 version = "2.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
-
-[[package]]
-name = "iri-string"
-version = "0.7.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20"
-dependencies = [
- "memchr",
- "serde",
-]
 
 [[package]]
 name = "is_terminal_polyfill"
@@ -1845,17 +2458,36 @@
 
 [[package]]
 name = "js-sys"
-version = "0.3.95"
+version = "0.3.102"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
+checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"
 dependencies = [
  "cfg-if",
  "futures-util",
- "once_cell",
  "wasm-bindgen",
 ]
 
 [[package]]
+name = "keccak"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+]
+
+[[package]]
+name = "kem"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd"
+dependencies = [
+ "crypto-common 0.2.2",
+ "rand_core 0.10.1",
+]
+
+[[package]]
 name = "lazy_static"
 version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1872,9 +2504,9 @@
 
 [[package]]
 name = "libc"
-version = "0.2.185"
+version = "0.2.186"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
 
 [[package]]
 name = "libloading"
@@ -1893,6 +2525,18 @@
 checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
 
 [[package]]
+name = "libredox"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3"
+dependencies = [
+ "bitflags",
+ "libc",
+ "plain",
+ "redox_syscall 0.8.1",
+]
+
+[[package]]
 name = "link-cplusplus"
 version = "1.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1936,9 +2580,9 @@
 
 [[package]]
 name = "log"
-version = "0.4.29"
+version = "0.4.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
 
 [[package]]
 name = "lru-slab"
@@ -1956,22 +2600,25 @@
 ]
 
 [[package]]
-name = "matchit"
-version = "0.7.3"
+name = "md5"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0"
 
 [[package]]
 name = "memchr"
-version = "2.8.0"
+version = "2.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
 
 [[package]]
-name = "mime"
-version = "0.3.17"
+name = "memoffset"
+version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
 
 [[package]]
 name = "minimal-lexical"
@@ -1991,9 +2638,9 @@
 
 [[package]]
 name = "mio"
-version = "1.2.0"
+version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
+checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
 dependencies = [
  "libc",
  "wasi",
@@ -2001,16 +2648,35 @@
 ]
 
 [[package]]
-name = "multimap"
-version = "0.10.1"
+name = "ml-kem"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66"
+dependencies = [
+ "hybrid-array",
+ "kem",
+ "module-lattice",
+ "pkcs8 0.11.0",
+ "rand_core 0.10.1",
+ "sha3",
+]
+
+[[package]]
+name = "module-lattice"
+version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
+checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe"
+dependencies = [
+ "ctutils",
+ "hybrid-array",
+ "num-traits",
+]
 
 [[package]]
 name = "nix"
-version = "0.31.2"
+version = "0.31.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
+checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
 dependencies = [
  "bitflags",
  "cfg-if",
@@ -2024,15 +2690,16 @@
 dependencies = [
  "anyhow",
  "bindgen",
+ "camino",
  "cxx",
  "cxx-build",
+ "futures",
  "itertools 0.14.0",
  "nixlike",
  "pkg-config",
  "serde",
  "serde_json",
  "test-log",
- "thiserror 2.0.18",
  "tokio",
  "tracing",
  "tracing-indicatif",
@@ -2040,6 +2707,13 @@
 ]
 
 [[package]]
+name = "nix-plugin-fleet"
+version = "0.1.0"
+dependencies = [
+ "fleet-base",
+]
+
+[[package]]
 name = "nixlike"
 version = "0.1.0"
 dependencies = [
@@ -2047,8 +2721,6 @@
  "linked-hash-map",
  "peg",
  "serde",
- "serde-transcode",
- "serde_json",
  "thiserror 2.0.18",
 ]
 
@@ -2081,6 +2753,16 @@
 ]
 
 [[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
 name = "num-bigint-dig"
 version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2098,9 +2780,9 @@
 
 [[package]]
 name = "num-conv"
-version = "0.2.1"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
 
 [[package]]
 name = "num-integer"
@@ -2151,20 +2833,6 @@
 checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
 
 [[package]]
-name = "openssh"
-version = "0.11.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d534c4bfecb0ed71dea4db444a5922a294d15cf40e700548f27295e1feb0ef18"
-dependencies = [
- "libc",
- "once_cell",
- "shell-escape",
- "tempfile",
- "thiserror 2.0.18",
- "tokio",
-]
-
-[[package]]
 name = "openssl-probe"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2229,12 +2897,12 @@
  "opentelemetry-http",
  "opentelemetry-proto",
  "opentelemetry_sdk",
- "prost 0.14.3",
+ "prost",
  "reqwest",
  "serde_json",
  "thiserror 2.0.18",
  "tokio",
- "tonic 0.14.5",
+ "tonic",
  "tracing",
 ]
 
@@ -2248,10 +2916,10 @@
  "const-hex",
  "opentelemetry",
  "opentelemetry_sdk",
- "prost 0.14.3",
+ "prost",
  "serde",
  "serde_json",
- "tonic 0.14.5",
+ "tonic",
  "tonic-prost",
 ]
 
@@ -2271,6 +2939,77 @@
 ]
 
 [[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "p256"
+version = "0.14.0-rc.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41adc63effe99d48837a8cc0e6d7a77e32ae6a07f6000df466178dbc2193093e"
+dependencies = [
+ "ecdsa",
+ "elliptic-curve",
+ "primefield",
+ "primeorder",
+ "sha2 0.11.0",
+]
+
+[[package]]
+name = "p384"
+version = "0.14.0-rc.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bd5333afa5ae0347f39e6a0f2c9c155da431583fd71fe5555bd0521b4ccaf02"
+dependencies = [
+ "ecdsa",
+ "elliptic-curve",
+ "fiat-crypto 0.3.0",
+ "primefield",
+ "primeorder",
+ "sha2 0.11.0",
+]
+
+[[package]]
+name = "p521"
+version = "0.14.0-rc.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3a5297f53dc16d35909060ba3032cff7867e8809f01e273ff325579d5f0ceae"
+dependencies = [
+ "base16ct",
+ "ecdsa",
+ "elliptic-curve",
+ "primefield",
+ "primeorder",
+ "sha2 0.11.0",
+]
+
+[[package]]
+name = "pageant"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f3a5ae18f65a85c67a77d18d42d3606c07948e3c17c1e5f74852b26589e88a5"
+dependencies = [
+ "base16ct",
+ "byteorder",
+ "bytes",
+ "delegate",
+ "futures",
+ "log",
+ "rand 0.10.1",
+ "sha2 0.11.0",
+ "thiserror 2.0.18",
+ "tokio",
+ "windows",
+ "windows-strings",
+]
+
+[[package]]
 name = "papergrid"
 version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2282,6 +3021,12 @@
 ]
 
 [[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
 name = "parking_lot"
 version = "0.12.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2299,26 +3044,51 @@
 dependencies = [
  "cfg-if",
  "libc",
- "redox_syscall",
+ "redox_syscall 0.5.18",
  "smallvec",
  "windows-link",
 ]
 
 [[package]]
+name = "password-hash"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b"
+dependencies = [
+ "phc",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
 name = "pbkdf2"
 version = "0.12.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
 dependencies = [
- "digest",
- "hmac",
+ "digest 0.10.7",
+ "hmac 0.12.1",
+]
+
+[[package]]
+name = "pbkdf2"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629"
+dependencies = [
+ "digest 0.11.3",
+ "hmac 0.13.0",
 ]
 
 [[package]]
 name = "peg"
-version = "0.8.5"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9928cfca101b36ec5163e70049ee5368a8a1c3c6efc9ca9c5f9cc2f816152477"
+checksum = "0aad070be5b63aa72103f2fcdd70a83adbd5e90112ce5b574171ff1c65501773"
 dependencies = [
  "peg-macros",
  "peg-runtime",
@@ -2326,9 +3096,9 @@
 
 [[package]]
 name = "peg-macros"
-version = "0.8.5"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6298ab04c202fa5b5d52ba03269fb7b74550b150323038878fe6c372d8280f71"
+checksum = "ddd8ef6825cae95355031ae26a99b616a2a21f22ba2de0197c43dfb05acbe7ee"
 dependencies = [
  "peg-runtime",
  "proc-macro2",
@@ -2337,18 +3107,17 @@
 
 [[package]]
 name = "peg-runtime"
-version = "0.8.5"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca"
+checksum = "7011d97b484a5ebdc4b1fdb3b12d5e4bbbea56e9d22b688f2e79e04b65a7d8a6"
 
 [[package]]
-name = "pem"
-version = "3.0.6"
+name = "pem-rfc7468"
+version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
+checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9"
 dependencies = [
- "base64 0.22.1",
- "serde_core",
+ "base64ct",
 ]
 
 [[package]]
@@ -2358,33 +3127,33 @@
 checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
 
 [[package]]
-name = "petgraph"
-version = "0.7.1"
+name = "phc"
+version = "0.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
+checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892"
 dependencies = [
- "fixedbitset",
- "indexmap 2.14.0",
+ "base64ct",
+ "ctutils",
 ]
 
 [[package]]
 name = "pin-project"
-version = "1.1.11"
+version = "1.1.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
+checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
 dependencies = [
  "pin-project-internal",
 ]
 
 [[package]]
 name = "pin-project-internal"
-version = "1.1.11"
+version = "1.1.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
+checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -2394,33 +3163,102 @@
 checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
 
 [[package]]
+name = "piper"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
+[[package]]
 name = "pkcs1"
 version = "0.7.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
 dependencies = [
- "der",
- "pkcs8",
- "spki",
+ "der 0.7.10",
+ "pkcs8 0.10.2",
+ "spki 0.7.3",
 ]
 
 [[package]]
+name = "pkcs1"
+version = "0.8.0-rc.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e"
+dependencies = [
+ "der 0.8.0",
+ "spki 0.8.0",
+]
+
+[[package]]
+name = "pkcs5"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "279a91971a1d8eb1260a30938eae3be9cb67b472dffecb222fbbbe2fd2dc1453"
+dependencies = [
+ "aes 0.9.1",
+ "cbc 0.2.1",
+ "der 0.8.0",
+ "pbkdf2 0.13.0",
+ "rand_core 0.10.1",
+ "scrypt 0.12.0",
+ "sha2 0.11.0",
+ "spki 0.8.0",
+]
+
+[[package]]
 name = "pkcs8"
 version = "0.10.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
 dependencies = [
- "der",
- "spki",
+ "der 0.7.10",
+ "spki 0.7.3",
 ]
 
 [[package]]
+name = "pkcs8"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7"
+dependencies = [
+ "der 0.8.0",
+ "pkcs5",
+ "rand_core 0.10.1",
+ "spki 0.8.0",
+]
+
+[[package]]
 name = "pkg-config"
 version = "0.3.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
 
 [[package]]
+name = "plain"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
+
+[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix 1.1.4",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
 name = "poly1305"
 version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2428,7 +3266,18 @@
 dependencies = [
  "cpufeatures 0.2.17",
  "opaque-debug",
- "universal-hash",
+ "universal-hash 0.5.1",
+]
+
+[[package]]
+name = "poly1305"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a00baa632505d05512f48a963e16051c54fda9a95cc9acea1a4e3c90991c4a2e"
+dependencies = [
+ "cpufeatures 0.3.0",
+ "universal-hash 0.6.1",
+ "zeroize",
 ]
 
 [[package]]
@@ -2440,7 +3289,18 @@
  "cfg-if",
  "cpufeatures 0.2.17",
  "opaque-debug",
- "universal-hash",
+ "universal-hash 0.5.1",
+]
+
+[[package]]
+name = "polyval"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dfc63250416fea14f5749b90725916a6c903f599d51cb635aa7a52bfd03eede"
+dependencies = [
+ "cpubits",
+ "cpufeatures 0.3.0",
+ "universal-hash 0.6.1",
 ]
 
 [[package]]
@@ -2480,10 +3340,42 @@
 checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
 dependencies = [
  "proc-macro2",
- "syn",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "primefield"
+version = "0.14.0-rc.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1d7e42f46a29abc16fb621a3466ee453358ebaae48a9e515f287e0af052ed8f"
+dependencies = [
+ "crypto-bigint",
+ "crypto-common 0.2.2",
+ "ff",
+ "rand_core 0.10.1",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "primeorder"
+version = "0.14.0-rc.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d2793f22b9b6fd11ef3ac1d59bf003c2573593e4968702341605c2748fd90bf"
+dependencies = [
+ "elliptic-curve",
 ]
 
 [[package]]
+name = "proc-macro-crate"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
+dependencies = [
+ "toml_edit",
+]
+
+[[package]]
 name = "proc-macro-error-attr2"
 version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2502,7 +3394,7 @@
  "proc-macro-error-attr2",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -2531,77 +3423,25 @@
 
 [[package]]
 name = "prost"
-version = "0.13.5"
+version = "0.14.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
+checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1"
 dependencies = [
  "bytes",
- "prost-derive 0.13.5",
-]
-
-[[package]]
-name = "prost"
-version = "0.14.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568"
-dependencies = [
- "bytes",
- "prost-derive 0.14.3",
+ "prost-derive",
 ]
 
 [[package]]
-name = "prost-build"
-version = "0.13.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
-dependencies = [
- "heck",
- "itertools 0.14.0",
- "log",
- "multimap",
- "once_cell",
- "petgraph",
- "prettyplease",
- "prost 0.13.5",
- "prost-types",
- "regex",
- "syn",
- "tempfile",
-]
-
-[[package]]
 name = "prost-derive"
-version = "0.13.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
-dependencies = [
- "anyhow",
- "itertools 0.14.0",
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "prost-derive"
-version = "0.14.3"
+version = "0.14.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
+checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"
 dependencies = [
  "anyhow",
  "itertools 0.14.0",
  "proc-macro2",
  "quote",
- "syn",
-]
-
-[[package]]
-name = "prost-types"
-version = "0.13.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
-dependencies = [
- "prost 0.13.5",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -2617,7 +3457,7 @@
  "quinn-udp",
  "rustc-hash 2.1.2",
  "rustls",
- "socket2 0.6.3",
+ "socket2",
  "thiserror 2.0.18",
  "tokio",
  "tracing",
@@ -2654,7 +3494,7 @@
  "cfg_aliases",
  "libc",
  "once_cell",
- "socket2 0.6.3",
+ "socket2",
  "tracing",
  "windows-sys 0.60.2",
 ]
@@ -2766,32 +3606,28 @@
 ]
 
 [[package]]
-name = "rcgen"
-version = "0.13.2"
+name = "redox_syscall"
+version = "0.5.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
 dependencies = [
- "pem",
- "ring",
- "rustls-pki-types",
- "time",
- "yasna",
+ "bitflags",
 ]
 
 [[package]]
 name = "redox_syscall"
-version = "0.5.18"
+version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7"
 dependencies = [
  "bitflags",
 ]
 
 [[package]]
 name = "regex"
-version = "1.12.3"
+version = "1.12.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -2812,18 +3648,131 @@
 
 [[package]]
 name = "regex-syntax"
-version = "0.8.10"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "remowt-client"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34d59c0ce311b4b3c094b0b196a1004aa438b9947c3eace2ca4d2a0ac08fac59"
+dependencies = [
+ "anyhow",
+ "bifrostlink",
+ "bifrostlink-ports",
+ "bytes",
+ "camino",
+ "futures",
+ "remowt-endpoints",
+ "remowt-link-shared",
+ "russh",
+ "russh-config",
+ "serde",
+ "serde_json",
+ "tempfile",
+ "tokio",
+ "tokio-util",
+ "tracing",
+ "uuid",
+]
+
+[[package]]
+name = "remowt-endpoints"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb203856cd5cff27504bd2023c59195b1f1ce1cf5d8ea836598993894eb1b9c0"
+dependencies = [
+ "anyhow",
+ "bifrostlink",
+ "bifrostlink-macros",
+ "camino",
+ "nix",
+ "serde",
+ "tempfile",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+ "uuid",
+ "zbus",
+]
+
+[[package]]
+name = "remowt-fleet"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "bifrostlink",
+ "camino",
+ "nix-eval",
+ "remowt-client",
+ "remowt-endpoints",
+ "serde",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+ "uuid",
+]
+
+[[package]]
+name = "remowt-link-shared"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98ebba5be028a2dfb7f1858bb0ff74037262753eaae4dc6e5d9ae241c4567668"
+dependencies = [
+ "bifrostlink",
+ "bytes",
+ "remowt-ui-prompt",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "remowt-plugin"
+version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+checksum = "ef73ef3eea5bb6152d01019b5062e51ef258a1ac1c7a6f8cdcdf6fe1eb192cdd"
+dependencies = [
+ "anyhow",
+ "bifrostlink",
+ "bifrostlink-ports",
+ "remowt-link-shared",
+ "serde_json",
+ "tokio",
+ "tracing",
+ "tracing-subscriber",
+]
 
 [[package]]
-name = "repl-plugin-unstable"
+name = "remowt-plugin-fleet"
 version = "0.1.0"
 dependencies = [
- "fleet-base",
+ "anyhow",
+ "remowt-fleet",
+ "remowt-plugin",
 ]
 
 [[package]]
+name = "remowt-ui-prompt"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acf28ba68e6f8087657157695e9ab42b82a21130e77f6dffd27781062109325d"
+dependencies = [
+ "anyhow",
+ "bifrostlink",
+ "bifrostlink-macros",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+ "zbus",
+]
+
+[[package]]
 name = "reqwest"
 version = "0.12.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2854,8 +3803,8 @@
  "sync_wrapper",
  "tokio",
  "tokio-rustls",
- "tower 0.5.3",
- "tower-http 0.6.8",
+ "tower",
+ "tower-http",
  "tower-service",
  "url",
  "wasm-bindgen",
@@ -2864,6 +3813,16 @@
 ]
 
 [[package]]
+name = "rfc6979"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5236ce872cac07e0fb3969b0cbf468c7d2f37d432f1b627dcb7b8d34563fb0c3"
+dependencies = [
+ "hmac 0.13.0",
+ "subtle",
+]
+
+[[package]]
 name = "ring"
 version = "0.17.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2878,45 +3837,155 @@
 ]
 
 [[package]]
-name = "rmp"
-version = "0.8.15"
+name = "rsa"
+version = "0.9.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
+checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
 dependencies = [
+ "const-oid 0.9.6",
+ "digest 0.10.7",
+ "num-bigint-dig",
+ "num-integer",
  "num-traits",
+ "pkcs1 0.7.5",
+ "pkcs8 0.10.2",
+ "rand_core 0.6.4",
+ "signature 2.2.0",
+ "spki 0.7.3",
+ "subtle",
+ "zeroize",
 ]
 
 [[package]]
-name = "rmp-serde"
-version = "1.3.1"
+name = "rsa"
+version = "0.10.0-rc.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
+checksum = "30b2aa4ba0d89f73d1e332df05be0eeab8840351c36ca5654341dfdb57bb3caf"
 dependencies = [
- "rmp",
- "serde",
+ "const-oid 0.10.2",
+ "crypto-bigint",
+ "crypto-primes",
+ "digest 0.11.3",
+ "pkcs1 0.8.0-rc.4",
+ "pkcs8 0.11.0",
+ "rand_core 0.10.1",
+ "sha2 0.11.0",
+ "signature 3.0.0",
+ "spki 0.8.0",
+ "zeroize",
 ]
 
 [[package]]
-name = "rsa"
-version = "0.9.10"
+name = "russh"
+version = "0.61.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
+checksum = "bbf893f64684e58da8a68d56a5e84d1cf0440226274c515770fe267707a7d0b0"
 dependencies = [
- "const-oid",
- "digest",
- "num-bigint-dig",
- "num-integer",
- "num-traits",
- "pkcs1",
- "pkcs8",
- "rand_core 0.6.4",
- "signature",
- "spki",
+ "aes 0.9.1",
+ "bitflags",
+ "block-padding 0.4.2",
+ "byteorder",
+ "bytes",
+ "cbc 0.2.1",
+ "cipher 0.5.2",
+ "crypto-bigint",
+ "ctr 0.10.1",
+ "curve25519-dalek 5.0.0-rc.0",
+ "data-encoding",
+ "delegate",
+ "der 0.8.0",
+ "digest 0.11.3",
+ "ecdsa",
+ "ed25519-dalek 3.0.0-rc.0",
+ "elliptic-curve",
+ "enum_dispatch",
+ "flate2",
+ "futures",
+ "generic-array 1.4.3",
+ "getrandom 0.4.2",
+ "ghash 0.6.0",
+ "hex-literal",
+ "hmac 0.13.0",
+ "inout 0.2.2",
+ "internal-russh-num-bigint",
+ "keccak",
+ "log",
+ "md5",
+ "ml-kem",
+ "module-lattice",
+ "num-bigint",
+ "p256",
+ "p384",
+ "p521",
+ "pageant",
+ "pbkdf2 0.13.0",
+ "pkcs1 0.8.0-rc.4",
+ "pkcs5",
+ "pkcs8 0.11.0",
+ "polyval 0.7.1",
+ "rand 0.10.1",
+ "rand_core 0.10.1",
+ "ring",
+ "rsa 0.10.0-rc.18",
+ "russh-cryptovec",
+ "russh-util",
+ "salsa20 0.11.0",
+ "scrypt 0.12.0",
+ "sec1",
+ "sha1",
+ "sha2 0.11.0",
+ "sha3",
+ "signature 3.0.0",
+ "spki 0.8.0",
+ "ssh-encoding",
+ "ssh-key",
  "subtle",
+ "thiserror 2.0.18",
+ "tokio",
+ "typenum",
+ "universal-hash 0.6.1",
  "zeroize",
 ]
 
 [[package]]
+name = "russh-config"
+version = "0.58.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "993346101d4741a59c82bafc6b645c52ce3e7b0820f13e4b5d48cdb591e3d955"
+dependencies = [
+ "futures",
+ "globset",
+ "log",
+ "thiserror 2.0.18",
+ "tokio",
+ "whoami",
+]
+
+[[package]]
+name = "russh-cryptovec"
+version = "0.61.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "443f6bbcfacb34a1aab2b12b99bf08e0c63abdc5a0db261901365df9d57fff51"
+dependencies = [
+ "log",
+ "nix",
+ "ssh-encoding",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "russh-util"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "668424a5dde0bcb45b55ba7de8476b93831b4aa2fa6947e145f3b053e22c60b6"
+dependencies = [
+ "chrono",
+ "tokio",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
 name = "rust-embed"
 version = "8.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2936,7 +4005,7 @@
  "proc-macro2",
  "quote",
  "rust-embed-utils",
- "syn",
+ "syn 2.0.117",
  "walkdir",
 ]
 
@@ -2946,7 +4015,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
 dependencies = [
- "sha2",
+ "sha2 0.10.9",
  "walkdir",
 ]
 
@@ -2999,11 +4068,10 @@
 
 [[package]]
 name = "rustls"
-version = "0.23.38"
+version = "0.23.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21"
+checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
 dependencies = [
- "log",
  "once_cell",
  "ring",
  "rustls-pki-types",
@@ -3014,30 +4082,21 @@
 
 [[package]]
 name = "rustls-native-certs"
-version = "0.8.3"
+version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
+checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
 dependencies = [
  "openssl-probe",
  "rustls-pki-types",
  "schannel",
  "security-framework",
-]
-
-[[package]]
-name = "rustls-pemfile"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
-dependencies = [
- "rustls-pki-types",
 ]
 
 [[package]]
 name = "rustls-pki-types"
-version = "1.14.0"
+version = "1.14.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
+checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
 dependencies = [
  "web-time",
  "zeroize",
@@ -3072,7 +4131,17 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
 dependencies = [
- "cipher",
+ "cipher 0.4.4",
+]
+
+[[package]]
+name = "salsa20"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c"
+dependencies = [
+ "cfg-if",
+ "cipher 0.5.2",
 ]
 
 [[package]]
@@ -3111,12 +4180,38 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
 dependencies = [
- "pbkdf2",
- "salsa20",
- "sha2",
+ "pbkdf2 0.12.2",
+ "salsa20 0.10.2",
+ "sha2 0.10.9",
 ]
 
 [[package]]
+name = "scrypt"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d87af57419b594aa23fa95f09f0e06d80d84ba01c26148c43844cad6ff4485f0"
+dependencies = [
+ "cfg-if",
+ "pbkdf2 0.13.0",
+ "salsa20 0.11.0",
+ "sha2 0.11.0",
+]
+
+[[package]]
+name = "sec1"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d"
+dependencies = [
+ "base16ct",
+ "ctutils",
+ "der 0.8.0",
+ "hybrid-array",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
 name = "secrecy"
 version = "0.10.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3177,28 +4272,9 @@
 dependencies = [
  "serde_core",
  "serde_derive",
-]
-
-[[package]]
-name = "serde-transcode"
-version = "1.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "590c0e25c2a5bb6e85bf5c1bce768ceb86b316e7a01bdf07d2cb4ec2271990e2"
-dependencies = [
- "serde",
 ]
 
 [[package]]
-name = "serde_bytes"
-version = "0.11.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
-dependencies = [
- "serde",
- "serde_core",
-]
-
-[[package]]
 name = "serde_core"
 version = "1.0.228"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3215,14 +4291,14 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.149"
+version = "1.0.150"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
 dependencies = [
  "itoa",
  "memchr",
@@ -3232,6 +4308,17 @@
 ]
 
 [[package]]
+name = "serde_repr"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
 name = "serde_urlencoded"
 version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3244,6 +4331,27 @@
 ]
 
 [[package]]
+name = "serdect"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e"
+dependencies = [
+ "base16ct",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "digest 0.11.3",
+]
+
+[[package]]
 name = "sha2"
 version = "0.10.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3251,10 +4359,31 @@
 dependencies = [
  "cfg-if",
  "cpufeatures 0.2.17",
- "digest",
+ "digest 0.10.7",
 ]
 
 [[package]]
+name = "sha2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "digest 0.11.3",
+]
+
+[[package]]
+name = "sha3"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1"
+dependencies = [
+ "digest 0.11.3",
+ "keccak",
+]
+
+[[package]]
 name = "sharded-slab"
 version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3264,16 +4393,16 @@
 ]
 
 [[package]]
-name = "shell-escape"
-version = "0.1.5"
+name = "shlex"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
 
 [[package]]
 name = "shlex"
-version = "1.3.0"
+version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
 
 [[package]]
 name = "signal-hook-registry"
@@ -3291,11 +4420,21 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
 dependencies = [
- "digest",
+ "digest 0.10.7",
  "rand_core 0.6.4",
 ]
 
 [[package]]
+name = "signature"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5"
+dependencies = [
+ "digest 0.11.3",
+ "rand_core 0.10.1",
+]
+
+[[package]]
 name = "simd-adler32"
 version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3309,25 +4448,15 @@
 
 [[package]]
 name = "smallvec"
-version = "1.15.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
-
-[[package]]
-name = "socket2"
-version = "0.5.10"
+version = "1.15.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
-dependencies = [
- "libc",
- "windows-sys 0.52.0",
-]
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
 
 [[package]]
 name = "socket2"
-version = "0.6.3"
+version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
+checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
 dependencies = [
  "libc",
  "windows-sys 0.61.2",
@@ -3346,10 +4475,81 @@
 checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
 dependencies = [
  "base64ct",
- "der",
+ "der 0.7.10",
+]
+
+[[package]]
+name = "spki"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f"
+dependencies = [
+ "base64ct",
+ "der 0.8.0",
+]
+
+[[package]]
+name = "ssh-cipher"
+version = "0.3.0-rc.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10db6f219196a8528f9ec904d9d45cdad692d65b0e57e72be4dedd1c5fddce36"
+dependencies = [
+ "aead 0.6.0",
+ "aes 0.9.1",
+ "aes-gcm 0.11.0-rc.4",
+ "cbc 0.2.1",
+ "chacha20 0.10.0",
+ "cipher 0.5.2",
+ "ctr 0.10.1",
+ "ctutils",
+ "des",
+ "poly1305 0.9.0",
+ "ssh-encoding",
+ "zeroize",
 ]
 
 [[package]]
+name = "ssh-encoding"
+version = "0.3.0-rc.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7abf34aa716da5d5b4c496936d042ea282ab392092cd68a72ef6a8863ff8c96a"
+dependencies = [
+ "base64ct",
+ "bytes",
+ "crypto-bigint",
+ "ctutils",
+ "digest 0.11.3",
+ "pem-rfc7468",
+ "zeroize",
+]
+
+[[package]]
+name = "ssh-key"
+version = "0.7.0-rc.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45735ce3dea95690e4a9e414c4cfde7f79835063c3dcd35881df85a84118e74b"
+dependencies = [
+ "argon2",
+ "bcrypt-pbkdf 0.11.0",
+ "ctutils",
+ "ed25519-dalek 3.0.0-rc.0",
+ "hex",
+ "hmac 0.13.0",
+ "p256",
+ "p384",
+ "p521",
+ "rand_core 0.10.1",
+ "rsa 0.10.0-rc.18",
+ "sec1",
+ "sha1",
+ "sha2 0.11.0",
+ "signature 3.0.0",
+ "ssh-cipher",
+ "ssh-encoding",
+ "zeroize",
+]
+
+[[package]]
 name = "stable_deref_trait"
 version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3369,6 +4569,17 @@
 
 [[package]]
 name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
 version = "2.0.117"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
@@ -3395,7 +4606,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -3419,7 +4630,7 @@
  "proc-macro-error2",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -3452,24 +4663,13 @@
 dependencies = [
  "rustix 1.1.4",
  "windows-sys 0.61.2",
-]
-
-[[package]]
-name = "terraform-provider-fleet"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "async-trait",
- "serde",
- "tf-provider",
- "tokio",
 ]
 
 [[package]]
 name = "test-log"
-version = "0.2.20"
+version = "0.2.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f46bf474f0a4afebf92f076d54fd5e63423d9438b8c278a3d2ccb0f47f7cdb3"
+checksum = "9b9c218384242b5c89b68303ab6f6fc53a312d923f0c14dc6bb860c6aeee40f1"
 dependencies = [
  "env_logger",
  "test-log-macros",
@@ -3478,22 +4678,22 @@
 
 [[package]]
 name = "test-log-core"
-version = "0.2.20"
+version = "0.2.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37d4d41320b48bc4a211a9021678fcc0c99569b594ea31c93735b8e517102b4c"
+checksum = "c26ef8b00e4d382e59f6a8ddb3cd790b3a5bb29f21a358a9a69ea2f29f13f27b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
 name = "test-log-macros"
-version = "0.2.20"
+version = "0.2.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9beb9249a81e430dffd42400a49019bcf548444f1968ff23080a625de0d4d320"
+checksum = "944ad38adcbb71eaa682c56bceeb079e4ca82b4b3edc2a0fde5cb297b77dac8d"
 dependencies = [
- "syn",
+ "syn 2.0.117",
  "test-log-core",
 ]
 
@@ -3504,34 +4704,6 @@
 checksum = "0f8daae29995a24f65619e19d8d31dea5b389f3d853d8bf297bbf607cd0014cc"
 dependencies = [
  "unicode-width",
-]
-
-[[package]]
-name = "tf-provider"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d80ea2e5f9f54717952d199888aab7e607dc99275ec5221f1259ce7a5f55f5a6"
-dependencies = [
- "anyhow",
- "async-stream",
- "async-trait",
- "base64 0.22.1",
- "futures",
- "prost 0.13.5",
- "rcgen",
- "rmp-serde",
- "serde",
- "serde_bytes",
- "serde_json",
- "time",
- "tokio",
- "tokio-stream",
- "tokio-util",
- "tonic 0.12.3",
- "tonic-build",
- "tower-http 0.5.2",
- "tracing",
- "tracing-subscriber",
 ]
 
 [[package]]
@@ -3560,7 +4732,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -3571,7 +4743,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -3585,9 +4757,9 @@
 
 [[package]]
 name = "time"
-version = "0.3.47"
+version = "0.3.49"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
+checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469"
 dependencies = [
  "deranged",
  "num-conv",
@@ -3599,15 +4771,15 @@
 
 [[package]]
 name = "time-core"
-version = "0.1.8"
+version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
 
 [[package]]
 name = "time-macros"
-version = "0.2.27"
+version = "0.2.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
+checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d"
 dependencies = [
  "num-conv",
  "time-core",
@@ -3641,16 +4813,16 @@
 
 [[package]]
 name = "tokio"
-version = "1.52.1"
+version = "1.52.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
 dependencies = [
  "bytes",
  "libc",
  "mio",
  "pin-project-lite",
  "signal-hook-registry",
- "socket2 0.6.3",
+ "socket2",
  "tokio-macros",
  "windows-sys 0.61.2",
 ]
@@ -3663,7 +4835,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -3685,7 +4857,6 @@
  "futures-core",
  "pin-project-lite",
  "tokio",
- "tokio-util",
 ]
 
 [[package]]
@@ -3712,24 +4883,23 @@
 
 [[package]]
 name = "toml_datetime"
-version = "0.7.5+spec-1.1.0"
+version = "1.1.1+spec-1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
 dependencies = [
  "serde_core",
 ]
 
 [[package]]
 name = "toml_edit"
-version = "0.23.10+spec-1.0.0"
+version = "0.25.12+spec-1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
+checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
 dependencies = [
- "indexmap 2.14.0",
+ "indexmap",
  "toml_datetime",
  "toml_parser",
- "toml_writer",
- "winnow 0.7.15",
+ "winnow",
 ]
 
 [[package]]
@@ -3738,52 +4908,14 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
 dependencies = [
- "winnow 1.0.2",
-]
-
-[[package]]
-name = "toml_writer"
-version = "1.1.1+spec-1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
-
-[[package]]
-name = "tonic"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
-dependencies = [
- "async-stream",
- "async-trait",
- "axum",
- "base64 0.22.1",
- "bytes",
- "h2",
- "http",
- "http-body",
- "http-body-util",
- "hyper",
- "hyper-timeout",
- "hyper-util",
- "percent-encoding",
- "pin-project",
- "prost 0.13.5",
- "rustls-pemfile",
- "socket2 0.5.10",
- "tokio",
- "tokio-rustls",
- "tokio-stream",
- "tower 0.4.13",
- "tower-layer",
- "tower-service",
- "tracing",
+ "winnow",
 ]
 
 [[package]]
 name = "tonic"
-version = "0.14.5"
+version = "0.14.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec"
+checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef"
 dependencies = [
  "async-trait",
  "base64 0.22.1",
@@ -3800,55 +4932,21 @@
  "sync_wrapper",
  "tokio",
  "tokio-stream",
- "tower 0.5.3",
+ "tower",
  "tower-layer",
  "tower-service",
  "tracing",
 ]
 
 [[package]]
-name = "tonic-build"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11"
-dependencies = [
- "prettyplease",
- "proc-macro2",
- "prost-build",
- "prost-types",
- "quote",
- "syn",
-]
-
-[[package]]
 name = "tonic-prost"
-version = "0.14.5"
+version = "0.14.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309"
+checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0"
 dependencies = [
  "bytes",
- "prost 0.14.3",
- "tonic 0.14.5",
-]
-
-[[package]]
-name = "tower"
-version = "0.4.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
-dependencies = [
- "futures-core",
- "futures-util",
- "indexmap 1.9.3",
- "pin-project",
- "pin-project-lite",
- "rand 0.8.6",
- "slab",
- "tokio",
- "tokio-util",
- "tower-layer",
- "tower-service",
- "tracing",
+ "prost",
+ "tonic",
 ]
 
 [[package]]
@@ -3859,7 +4957,7 @@
 dependencies = [
  "futures-core",
  "futures-util",
- "indexmap 2.14.0",
+ "indexmap",
  "pin-project-lite",
  "slab",
  "sync_wrapper",
@@ -3872,37 +4970,20 @@
 
 [[package]]
 name = "tower-http"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
-dependencies = [
- "bitflags",
- "bytes",
- "http",
- "http-body",
- "http-body-util",
- "pin-project-lite",
- "tower-layer",
- "tower-service",
- "tracing",
-]
-
-[[package]]
-name = "tower-http"
-version = "0.6.8"
+version = "0.6.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
 dependencies = [
  "bitflags",
  "bytes",
  "futures-util",
  "http",
  "http-body",
- "iri-string",
  "pin-project-lite",
- "tower 0.5.3",
+ "tower",
  "tower-layer",
  "tower-service",
+ "url",
 ]
 
 [[package]]
@@ -3936,7 +5017,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -3989,16 +5070,6 @@
 ]
 
 [[package]]
-name = "tracing-serde"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
-dependencies = [
- "serde",
- "tracing-core",
-]
-
-[[package]]
 name = "tracing-subscriber"
 version = "0.3.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4008,15 +5079,12 @@
  "nu-ansi-term",
  "once_cell",
  "regex-automata",
- "serde",
- "serde_json",
  "sharded-slab",
  "smallvec",
  "thread_local",
  "tracing",
  "tracing-core",
  "tracing-log",
- "tracing-serde",
 ]
 
 [[package]]
@@ -4036,11 +5104,22 @@
 
 [[package]]
 name = "typenum"
-version = "1.20.0"
+version = "1.20.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
 
 [[package]]
+name = "uds_windows"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
+dependencies = [
+ "memoffset",
+ "tempfile",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
 name = "unarray"
 version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4107,11 +5186,21 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
 dependencies = [
- "crypto-common",
+ "crypto-common 0.1.7",
  "subtle",
 ]
 
 [[package]]
+name = "universal-hash"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96"
+dependencies = [
+ "crypto-common 0.2.2",
+ "ctutils",
+]
+
+[[package]]
 name = "untrusted"
 version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4142,6 +5231,18 @@
 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 
 [[package]]
+name = "uuid"
+version = "1.23.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"
+dependencies = [
+ "getrandom 0.4.2",
+ "js-sys",
+ "serde_core",
+ "wasm-bindgen",
+]
+
+[[package]]
 name = "valuable"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4204,9 +5305,9 @@
 
 [[package]]
 name = "wasip2"
-version = "1.0.3+wasi-0.2.9"
+version = "1.0.4+wasi-0.2.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
 dependencies = [
  "wit-bindgen 0.57.1",
 ]
@@ -4221,10 +5322,16 @@
 ]
 
 [[package]]
+name = "wasite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
+
+[[package]]
 name = "wasm-bindgen"
-version = "0.2.118"
+version = "0.2.125"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
+checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
 dependencies = [
  "cfg-if",
  "once_cell",
@@ -4235,9 +5342,9 @@
 
 [[package]]
 name = "wasm-bindgen-futures"
-version = "0.4.68"
+version = "0.4.75"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8"
+checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -4245,9 +5352,9 @@
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.118"
+version = "0.2.125"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
+checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -4255,22 +5362,22 @@
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.118"
+version = "0.2.125"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
+checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
 dependencies = [
  "bumpalo",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.118"
+version = "0.2.125"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
+checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
 dependencies = [
  "unicode-ident",
 ]
@@ -4292,7 +5399,7 @@
 checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
 dependencies = [
  "anyhow",
- "indexmap 2.14.0",
+ "indexmap",
  "wasm-encoder",
  "wasmparser",
 ]
@@ -4305,15 +5412,15 @@
 dependencies = [
  "bitflags",
  "hashbrown 0.15.5",
- "indexmap 2.14.0",
+ "indexmap",
  "semver",
 ]
 
 [[package]]
 name = "web-sys"
-version = "0.3.95"
+version = "0.3.102"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d"
+checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -4342,6 +5449,17 @@
 ]
 
 [[package]]
+name = "whoami"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
+dependencies = [
+ "libredox",
+ "wasite",
+ "web-sys",
+]
+
+[[package]]
 name = "winapi-util"
 version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4351,6 +5469,27 @@
 ]
 
 [[package]]
+name = "windows"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
+dependencies = [
+ "windows-collections",
+ "windows-core",
+ "windows-future",
+ "windows-numerics",
+]
+
+[[package]]
+name = "windows-collections"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
+dependencies = [
+ "windows-core",
+]
+
+[[package]]
 name = "windows-core"
 version = "0.62.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4364,6 +5503,17 @@
 ]
 
 [[package]]
+name = "windows-future"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
+dependencies = [
+ "windows-core",
+ "windows-link",
+ "windows-threading",
+]
+
+[[package]]
 name = "windows-implement"
 version = "0.60.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4371,7 +5521,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -4382,7 +5532,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -4392,6 +5542,16 @@
 checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
 
 [[package]]
+name = "windows-numerics"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
+dependencies = [
+ "windows-core",
+ "windows-link",
+]
+
+[[package]]
 name = "windows-result"
 version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4479,6 +5639,15 @@
 ]
 
 [[package]]
+name = "windows-threading"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
 name = "windows_aarch64_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4576,20 +5745,14 @@
 
 [[package]]
 name = "winnow"
-version = "0.7.15"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
 dependencies = [
  "memchr",
 ]
 
 [[package]]
-name = "winnow"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0"
-
-[[package]]
 name = "wit-bindgen"
 version = "0.51.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4623,9 +5786,9 @@
 dependencies = [
  "anyhow",
  "heck",
- "indexmap 2.14.0",
+ "indexmap",
  "prettyplease",
- "syn",
+ "syn 2.0.117",
  "wasm-metadata",
  "wit-bindgen-core",
  "wit-component",
@@ -4641,7 +5804,7 @@
  "prettyplease",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
  "wit-bindgen-core",
  "wit-bindgen-rust",
 ]
@@ -4654,7 +5817,7 @@
 dependencies = [
  "anyhow",
  "bitflags",
- "indexmap 2.14.0",
+ "indexmap",
  "log",
  "serde",
  "serde_derive",
@@ -4673,7 +5836,7 @@
 dependencies = [
  "anyhow",
  "id-arena",
- "indexmap 2.14.0",
+ "indexmap",
  "log",
  "semver",
  "serde",
@@ -4701,26 +5864,17 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
 dependencies = [
- "curve25519-dalek",
+ "curve25519-dalek 4.1.3",
  "rand_core 0.6.4",
  "serde",
  "zeroize",
 ]
 
 [[package]]
-name = "yasna"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
-dependencies = [
- "time",
-]
-
-[[package]]
 name = "yoke"
-version = "0.8.2"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
 dependencies = [
  "stable_deref_trait",
  "yoke-derive",
@@ -4735,35 +5889,96 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
  "synstructure",
 ]
 
 [[package]]
+name = "zbus"
+version = "5.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-lite",
+ "hex",
+ "libc",
+ "ordered-stream",
+ "rustix 1.1.4",
+ "serde",
+ "serde_repr",
+ "tracing",
+ "uds_windows",
+ "uuid",
+ "windows-sys 0.61.2",
+ "winnow",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "5.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "zbus_names",
+ "zvariant",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "4.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d"
+dependencies = [
+ "serde",
+ "winnow",
+ "zvariant",
+]
+
+[[package]]
 name = "zerocopy"
-version = "0.8.48"
+version = "0.8.52"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
+checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
 dependencies = [
  "zerocopy-derive",
 ]
 
 [[package]]
 name = "zerocopy-derive"
-version = "0.8.48"
+version = "0.8.52"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
+checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
 name = "zerofrom"
-version = "0.1.7"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
 dependencies = [
  "zerofrom-derive",
 ]
@@ -4776,28 +5991,28 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
  "synstructure",
 ]
 
 [[package]]
 name = "zeroize"
-version = "1.8.2"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
 dependencies = [
  "zeroize_derive",
 ]
 
 [[package]]
 name = "zeroize_derive"
-version = "1.4.3"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
+checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -4831,7 +6046,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -4839,3 +6054,43 @@
 version = "1.0.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
+[[package]]
+name = "zvariant"
+version = "5.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "winnow",
+ "zvariant_derive",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "5.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 2.0.117",
+ "winnow",
+]
modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,22 +6,28 @@
 package.rust-version = "1.89.0"
 
 [workspace.dependencies]
-better-command = { path = "./crates/better-command" }
 fleet-base = { path = "./crates/fleet-base" }
 fleet-shared = { path = "./crates/fleet-shared" }
 nix-eval = { path = "./crates/nix-eval" }
 nixlike = { path = "./crates/nixlike" }
 opentelemetry-exporter-env = { path = "./crates/opentelemetry-exporter-env" }
+remowt-fleet = { path = "./crates/remowt-fleet" }
 
-abort-on-drop = "0.2"
+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"
+
+bifrostlink = "0.2.3"
+
+uuid = { version = "1", features = ["v4"] }
+
 age = { version = "0.11", features = ["plugin", "ssh"] }
-age-core = "0.11"
 anyhow = "1.0"
-async-trait = "0.1.88"
-axum = { version = "0.8.7", features = ["http2", "macros", "ws"] }
-axum-extra = { version = "0.12.2", features = ["typed-header"] }
 base64 = "0.22.1"
 bindgen = "0.72.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"
@@ -29,19 +35,16 @@
 cxx-build = "1.0.168"
 ed25519-dalek = "2.1"
 futures = "0.3.31"
-futures-util = { version = "0.3.31", features = ["sink"] }
 hex = "0.4.3"
 hmac = "0.12"
 hostname = "0.4.1"
 human-repr = "1.1"
-hyper = "1.8.1"
 indicatif = "0.18"
 indoc = "2.0.6"
 itertools = "0.14.0"
 linked-hash-map = "0.5.6"
 nix = { version = "0.31.2", features = ["fs", "user"] }
 nom = "8.0.0"
-openssh = "0.11.5"
 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"] }
@@ -50,21 +53,16 @@
 peg = "0.8.5"
 pkg-config = "0.3.30"
 rand = "0.10.0"
-regex = "1.11"
 serde = { version = "1.0", features = ["derive"] }
-serde-transcode = "1.1.1"
 serde_json = "1.0"
 sha2 = "0.10"
 shlex = "1.3"
 tabled = "0.20.0"
 tempfile = "3.20"
 test-log = { version = "0.2.19", features = ["trace"] }
-tf-provider = "0.2.2"
 thiserror = "2.0.12"
 time = "0.3.41"
 tokio = { version = "1.45.1", features = ["fs", "macros", "rt", "rt-multi-thread", "sync", "time"] }
-tokio-util = "0.7.17"
-toml_edit = "0.23.10"
 tracing = "0.1"
 tracing-indicatif = "0.3.13"
 tracing-opentelemetry = "0.32.1"
modifiedcmds/fleet/Cargo.tomldiffbeforeafterboth
--- a/cmds/fleet/Cargo.toml
+++ b/cmds/fleet/Cargo.toml
@@ -8,15 +8,13 @@
 default-run = "fleet"
 
 [dependencies]
-age = { workspace = true, features = ["armor"] }
 anyhow.workspace = true
-better-command.workspace = true
+camino.workspace = true
 clap.workspace = true
 clap_complete.workspace = true
 fleet-base.workspace = true
-fleet-shared.workspace = true
 nix-eval.workspace = true
-nixlike.workspace = true
+remowt-fleet.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 tempfile.workspace = true
@@ -24,30 +22,17 @@
 tracing.workspace = true
 tracing-subscriber.workspace = true
 
-abort-on-drop.workspace = true
-age-core.workspace = true
-async-trait.workspace = true
-base64.workspace = true
-chrono.workspace = true
 futures.workspace = true
-hostname.workspace = true
 itertools.workspace = true
-openssh.workspace = true
-peg.workspace = true
-regex.workspace = true
 shlex.workspace = true
 tabled.workspace = true
-time = { workspace = true, features = ["serde"] }
-tokio-util = { workspace = true, features = ["codec"] }
 
 human-repr = { workspace = true, optional = true }
 indicatif = { workspace = true, optional = true }
-nom.workspace = true
 opentelemetry.workspace = true
 opentelemetry-appender-tracing.workspace = true
 opentelemetry-exporter-env.workspace = true
 opentelemetry_sdk.workspace = true
-thiserror.workspace = true
 tracing-indicatif = { workspace = true, optional = true }
 tracing-opentelemetry.workspace = true
 
@@ -58,6 +43,5 @@
 	"dep:tracing-indicatif",
 	"dep:indicatif",
 	"dep:human-repr",
-	"better-command/indicatif",
 	"nix-eval/indicatif",
 ]
modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -1,6 +1,7 @@
 use std::{env::current_dir, os::unix::fs::symlink, path::PathBuf};
 
 use anyhow::Result;
+use camino::Utf8PathBuf;
 use clap::Parser;
 use fleet_base::{
 	deploy::{DeployAction, deploy_task, upload_task},
@@ -29,7 +30,7 @@
 	build_attr: String,
 }
 
-async fn build_task(config: Config, hostname: String, build_attr: &str) -> Result<PathBuf> {
+async fn build_task(config: Config, hostname: String, build_attr: &str) -> Result<Utf8PathBuf> {
 	info!("building");
 	let host = config.host(&hostname)?;
 	// let action = Action::from(self.subcommand.clone());
@@ -42,17 +43,20 @@
 	// We already have system profiles for backups.
 	if !host.local {
 		info!("adding gc root");
-		let mut cmd = config.local_host().cmd("nix").await?;
-		cmd.arg("build")
-			.comparg(
-				"--profile",
-				format!(
-					"/nix/var/nix/profiles/{}-{hostname}",
-					config.data.gc_root_prefix
-				),
-			)
-			.arg(&out_output);
-		cmd.sudo().run_nix().await?;
+		let local = config.local_host();
+		let plugin_id = local.ensure_nix_plugin().await?;
+		let nix = local
+			.remowt()
+			.await?
+			.plugin_endpoints::<remowt_fleet::NixClient<_>>(plugin_id);
+		let profile = format!(
+			"/nix/var/nix/profiles/{}-{hostname}",
+			config.data.gc_root_prefix
+		);
+		nix.switch_profile(profile, out_output.clone())
+			.await
+			.map_err(|e| anyhow::anyhow!("{e:?}"))?
+			.map_err(|e| anyhow::anyhow!("{e}"))?;
 	}
 
 	Ok(out_output)
modifiedcmds/fleet/src/cmds/tf.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/tf.rs
+++ b/cmds/fleet/src/cmds/tf.rs
@@ -1,6 +1,7 @@
-use std::{collections::BTreeMap, ffi::OsString, path::PathBuf};
+use std::{collections::BTreeMap, ffi::OsString};
 
 use anyhow::{Context, Result};
+use camino::Utf8PathBuf;
 use clap::Parser;
 use fleet_base::host::Config;
 use nix_eval::nix_go;
@@ -40,7 +41,7 @@
 			let system = &config.local_system;
 			let config = &config.flake_outputs;
 			let data = nix_go!(config.tf({ system }));
-			let data: PathBuf = spawn_blocking(move || data.build("out"))
+			let data: Utf8PathBuf = spawn_blocking(move || data.build("out"))
 				.await
 				.expect("tf.json derivation should not fail")?;
 			let data = fs::read(&data).await?;
modifiedcmds/fleet/src/main.rsdiffbeforeafterboth
--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -4,9 +4,10 @@
 // pub(crate) mod command;
 pub(crate) mod extra_args;
 
-use std::{env, ffi::OsString, process::ExitCode, sync::Arc};
+use std::{process::ExitCode, sync::Arc};
 
-use anyhow::{Result, bail};
+use anyhow::{Context as _, Result, bail};
+use camino::Utf8PathBuf;
 use clap::{CommandFactory, Parser};
 use cmds::{
 	build_systems::{BuildSystems, Deploy},
@@ -23,7 +24,8 @@
 #[cfg(feature = "indicatif")]
 use indicatif::{ProgressState, ProgressStyle};
 use nix_eval::{
-	gc_register_my_thread, gc_unregister_my_thread, init_libraries, init_tokio_for_nix,
+	add_file_to_store, gc_register_my_thread, gc_unregister_my_thread, init_libraries,
+	init_tokio_for_nix,
 };
 use opentelemetry::trace::TracerProvider;
 use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
@@ -48,24 +50,21 @@
 		}
 		let tasks = FuturesUnordered::new();
 		for entry in std::fs::read_dir(&prefetch_dir)? {
-			tasks.push(async {
-				let entry = entry?;
-				if !entry.metadata()?.is_file() {
-					bail!("only files should exist in prefetch directory");
-				}
-				let span = info_span!(
-					"prefetching",
-					name = entry.file_name().to_string_lossy().as_ref()
-				);
-				let mut path = OsString::new();
-				path.push("file://");
-				path.push(entry.path());
-
-				let mut status = config.local_host().cmd("nix").await?;
-				status.args(&config.nix_args);
-				status.arg("store").arg("prefetch-file").arg(path);
-				status.run_nix_string().instrument(span).await?;
-				Ok(())
+			let entry = entry?;
+			if !entry.metadata()?.is_file() {
+				bail!("only files should exist in prefetch directory");
+			}
+			let name = entry.file_name().to_string_lossy().into_owned();
+			let path =
+				Utf8PathBuf::try_from(entry.path()).context("prefetch path should be utf8")?;
+			let span = info_span!("prefetching", name = %name);
+			tasks.push(async move {
+				let added = tokio::task::spawn_blocking(move || add_file_to_store(&name, &path))
+					.instrument(span.clone())
+					.await??;
+				let _g = span.enter();
+				info!("{} -> {}", added.hash, added.store_path);
+				anyhow::Ok(())
 			});
 		}
 		tasks.try_collect::<Vec<()>>().await?;
@@ -179,9 +178,7 @@
 	let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
 
 	let reg = tracing_subscriber::registry().with({
-		let sub = tracing_subscriber::fmt::layer()
-			.without_time()
-			.with_target(false);
+		let sub = tracing_subscriber::fmt::layer().without_time();
 		#[cfg(feature = "indicatif")]
 		let sub = sub.with_writer(indicatif_layer.get_stderr_writer());
 		sub.with_filter(filter) // .without,
@@ -262,14 +259,10 @@
 }
 
 async fn main_real(opts: RootOpts) -> Result<()> {
-	let nix_args = std::env::var_os("NIX_ARGS")
-		.map(|a| extra_args::parse_os(&a))
-		.transpose()?
-		.unwrap_or_default();
-	let config = opts.fleet_opts.build(
-		nix_args,
-		matches!(opts.command, Opts::Deploy(_) | Opts::BuildSystems(_)),
-	)?;
+	let config = opts.fleet_opts.build(matches!(
+		opts.command,
+		Opts::Deploy(_) | Opts::BuildSystems(_)
+	))?;
 
 	match run_command(&config, opts.fleet_opts, opts.command).await {
 		Ok(()) => {
modifiedcmds/install-secrets/src/main.rsdiffbeforeafterboth
--- a/cmds/install-secrets/src/main.rs
+++ b/cmds/install-secrets/src/main.rs
@@ -227,7 +227,6 @@
 				.from_env_lossy(),
 		)
 		.without_time()
-		.with_target(false)
 		.init();
 
 	let opts = Opts::parse();
addedcmds/nix-plugin-fleet/Cargo.tomldiffbeforeafterboth
--- /dev/null
+++ b/cmds/nix-plugin-fleet/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "nix-plugin-fleet"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+fleet-base.workspace = true
addedcmds/nix-plugin-fleet/src/lib.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/nix-plugin-fleet/src/lib.rs
@@ -0,0 +1,7 @@
+use fleet_base::primops::init_primops;
+
+/// SAFETY: expected plugin dynamic library entry point
+#[unsafe(no_mangle)]
+fn nix_plugin_entry() {
+	init_primops();
+}
addedcmds/remowt-plugin-fleet/Cargo.tomldiffbeforeafterboth
--- /dev/null
+++ b/cmds/remowt-plugin-fleet/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "remowt-plugin-fleet"
+description = "Remowt plugin exposing fleet's Nix endpoint to a running agent"
+version.workspace = true
+edition = "2021"
+
+[dependencies]
+remowt-fleet.workspace = true
+remowt-plugin.workspace = true
+
+anyhow.workspace = true
addedcmds/remowt-plugin-fleet/src/main.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/remowt-plugin-fleet/src/main.rs
@@ -0,0 +1,8 @@
+use remowt_fleet::{Nix, init_libraries};
+
+fn main() -> anyhow::Result<()> {
+	remowt_plugin::run(|rpc| {
+		init_libraries();
+		Nix.register_endpoints(rpc);
+	})
+}
deletedcmds/repl-plugin-unstable/Cargo.tomldiffbeforeafterboth
--- a/cmds/repl-plugin-unstable/Cargo.toml
+++ /dev/null
@@ -1,11 +0,0 @@
-[package]
-name = "repl-plugin-unstable"
-version.workspace = true
-edition.workspace = true
-rust-version.workspace = true
-
-[lib]
-crate-type = ["cdylib"]
-
-[dependencies]
-fleet-base.workspace = true
deletedcmds/repl-plugin-unstable/src/lib.rsdiffbeforeafterboth
--- a/cmds/repl-plugin-unstable/src/lib.rs
+++ /dev/null
@@ -1,7 +0,0 @@
-use fleet_base::primops::init_primops;
-
-/// SAFETY: expected plugin dynamic library entry point
-#[unsafe(no_mangle)]
-fn nix_plugin_entry() {
-	init_primops();
-}
deletedcmds/terraform-provider-fleet/Cargo.tomldiffbeforeafterboth
--- a/cmds/terraform-provider-fleet/Cargo.toml
+++ /dev/null
@@ -1,13 +0,0 @@
-[package]
-name = "terraform-provider-fleet"
-version.workspace = true
-edition.workspace = true
-rust-version.workspace = true
-
-[dependencies]
-anyhow.workspace = true
-serde = { workspace = true, features = ["derive"] }
-tokio.workspace = true
-
-async-trait.workspace = true
-tf-provider.workspace = true
deletedcmds/terraform-provider-fleet/src/main.rsdiffbeforeafterboth
--- a/cmds/terraform-provider-fleet/src/main.rs
+++ /dev/null
@@ -1,5 +0,0 @@
-fn main() {
-	panic!(
-		"this is a stub, real provider is in development, I just don't want to keep it in different branch."
-	)
-}
deletedcrates/better-command/Cargo.tomldiffbeforeafterboth
--- a/crates/better-command/Cargo.toml
+++ /dev/null
@@ -1,16 +0,0 @@
-[package]
-name = "better-command"
-version.workspace = true
-edition.workspace = true
-rust-version.workspace = true
-
-[dependencies]
-regex.workspace = true
-serde.workspace = true
-serde_json.workspace = true
-tracing.workspace = true
-
-tracing-indicatif = { workspace = true, optional = true }
-
-[features]
-indicatif = ["tracing-indicatif"]
deletedcrates/better-command/src/handler.rsdiffbeforeafterboth
--- a/crates/better-command/src/handler.rs
+++ /dev/null
@@ -1,335 +0,0 @@
-//! Collection of handlers, which transform program-specific stdout format to tracing
-
-use std::{
-	collections::HashMap,
-	sync::{Arc, LazyLock, Mutex},
-};
-
-use regex::Regex;
-use serde::Deserialize;
-use tracing::{Span, info, info_span, warn};
-#[cfg(feature = "indicatif")]
-use tracing_indicatif::span_ext::IndicatifSpanExt as _;
-
-pub trait Handler: Send {
-	fn handle_line(&mut self, e: &str);
-}
-
-/// Handler wrapper, which can be cloned.
-pub struct ClonableHandler<H>(Arc<Mutex<H>>);
-impl<H> Clone for ClonableHandler<H> {
-	fn clone(&self) -> Self {
-		Self(self.0.clone())
-	}
-}
-impl<H> ClonableHandler<H> {
-	pub fn new(inner: H) -> Self {
-		Self(Arc::new(Mutex::new(inner)))
-	}
-}
-impl<H: Handler> Handler for ClonableHandler<H> {
-	fn handle_line(&mut self, e: &str) {
-		self.0.lock().unwrap().handle_line(e)
-	}
-}
-
-/// Converts command output to tracing lines
-pub struct PlainHandler;
-impl Handler for PlainHandler {
-	fn handle_line(&mut self, e: &str) {
-		info!(target: "log", "{e}");
-	}
-}
-
-/// Ignores output
-pub struct NoopHandler;
-impl Handler for NoopHandler {
-	fn handle_line(&mut self, _e: &str) {}
-}
-
-/// Transform nix internal-json logs to tracing spans.
-#[derive(Default)]
-pub struct NixHandler {
-	spans: HashMap<u64, Span>,
-}
-#[derive(Deserialize, Debug)]
-#[serde(untagged)]
-enum LogField {
-	String(String),
-	Num(u64),
-}
-
-/// Nix internal-json log line type
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase", tag = "action")]
-#[allow(dead_code)]
-enum NixLog {
-	Msg {
-		level: u32,
-		msg: String,
-		raw_msg: Option<String>,
-	},
-	Start {
-		id: u64,
-		level: u32,
-		#[serde(default)]
-		fields: Vec<LogField>,
-		text: String,
-		#[serde(rename = "type")]
-		typ: u32,
-	},
-	Stop {
-		id: u64,
-	},
-	Result {
-		id: u64,
-		#[serde(rename = "type")]
-		typ: u32,
-		#[serde(default)]
-		fields: Vec<LogField>,
-	},
-}
-fn process_message(m: &str) -> String {
-	// Supposed to remove formatting characters except colors, as some programs try to reset cursor position etc.
-	static OSC_CLEANER: LazyLock<Regex> =
-		LazyLock::new(|| Regex::new(r"\x1B\]([^\x07\x1C]*[\x07\x1C])?|\r").unwrap());
-	static DETABBER: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\t").unwrap());
-	let m = OSC_CLEANER.replace_all(m, "");
-	// Indicatif can't format tabs. This is not the correct tab formatting, as correct one should be aligned,
-	// and not just be replaced with the constant number of spaces, but it's ok for now, as statuses are single-line.
-	DETABBER.replace_all(m.as_ref(), "  ").to_string()
-}
-impl Handler for NixHandler {
-	fn handle_line(&mut self, e: &str) {
-		if let Some(e) = e.strip_prefix("@nix ") {
-			let log: NixLog = match serde_json::from_str(e) {
-				Ok(l) => l,
-				Err(err) => {
-					warn!("failed to parse nix log line {:?}: {}", e, err);
-					return;
-				}
-			};
-			match log {
-				NixLog::Msg { msg, raw_msg, .. } => {
-					#[allow(clippy::nonminimal_bool)]
-					if !(msg.starts_with("\u{1b}[35;1mwarning:\u{1b}[0m Git tree '")
-						&& msg.ends_with("' is dirty"))
-						&& !msg.starts_with(
-							"\u{1b}[35;1mwarning:\u{1b}[0m not writing modified lock file of flake",
-						) && msg
-						!= "\u{1b}[35;1mwarning:\u{1b}[0m \u{1b}[31;1merror:\u{1b}[0m SQLite database '\u{1b}[35;1m/nix/var/nix/db/db.sqlite\u{1b}[0m' is busy"
-					{
-						if let Some(raw_msg) = raw_msg {
-							if !msg.is_empty() {
-								info!(target: "nix", "{}\n{}", raw_msg.trim_end(), msg.trim_end())
-							} else {
-								info!(target: "nix", "{}", raw_msg.trim_end())
-							}
-						} else {
-							info!(target: "nix", "{}", msg.trim_end())
-						}
-					}
-				}
-				NixLog::Start {
-					ref fields,
-					typ,
-					id,
-					..
-				} if typ == 105 && !fields.is_empty() => {
-					if let [LogField::String(drv), ..] = &fields[..] {
-						let mut drv = drv.as_str();
-						if let Some(pkg) = drv.strip_prefix("/nix/store/") {
-							let mut it = pkg.splitn(2, '-');
-							it.next();
-							if let Some(pkg) = it.next() {
-								drv = pkg;
-							}
-						}
-						info!(target: "nix","building {}", drv);
-						let span = info_span!("build", drv);
-						#[cfg(feature = "indicatif")]
-						span.pb_start();
-						self.spans.insert(id, span);
-					} else {
-						warn!("bad build log: {:?}", log)
-					}
-				}
-				NixLog::Start {
-					ref fields,
-					typ,
-					id,
-					..
-				} if typ == 100 && fields.len() >= 3 => {
-					if let [
-						LogField::String(drv),
-						LogField::String(from),
-						LogField::String(to),
-						..,
-					] = &fields[..]
-					{
-						let mut drv = drv.as_str();
-
-						if let Some(pkg) = drv.strip_prefix("/nix/store/") {
-							let mut it = pkg.splitn(2, '-');
-							it.next();
-							if let Some(pkg) = it.next() {
-								drv = pkg;
-							}
-						}
-						info!(target: "nix","copying {} {} -> {}", drv, from, to);
-						let span = info_span!("copy", from, to, drv);
-						#[cfg(feature = "indicatif")]
-						span.pb_start();
-						self.spans.insert(id, span);
-					} else {
-						warn!("bad copy log: {:?}", log)
-					}
-				}
-				NixLog::Start { text, typ, id, .. }
-					if typ == 0 || typ == 102 || typ == 103 || typ == 104 =>
-				{
-					if !text.is_empty()
-						&& text != "querying info about missing paths"
-						&& text != "copying 0 paths"
-						// Too much spam on lazy-trees branch
-						&& !(text.starts_with("copying '") && text.ends_with("' to the store"))
-					{
-						let span = info_span!("job");
-						#[cfg(feature = "indicatif")]
-						{
-							span.pb_start();
-							span.pb_set_message(&process_message(text.trim()));
-						}
-						self.spans.insert(id, span);
-						info!(target: "nix", "{}", text);
-					}
-				}
-				NixLog::Start {
-					text,
-					level: 0,
-					typ: 108,
-					..
-				} if text.is_empty() => {
-					// Cache lookup? Coupled with copy log
-				}
-				NixLog::Start {
-					text,
-					level: 4,
-					typ: 109,
-					..
-				} if text.starts_with("querying info about ") => {
-					// Cache lookup
-				}
-				NixLog::Start {
-					text,
-					level: 4,
-					typ: 101,
-					..
-				} if text.starts_with("downloading ") => {
-					// NAR downloading, coupled with copy log
-				}
-				NixLog::Start {
-					text,
-					level: 1,
-					typ: 111,
-					..
-				} if text.starts_with("waiting for a machine to build ") => {
-					// Useless repeating notification about build
-				}
-				NixLog::Start {
-					text,
-					level: 3,
-					typ: 111,
-					..
-				} if text.starts_with("resolved derivation: ") => {
-					// CA resolved
-				}
-				NixLog::Start {
-					text,
-					level: 1,
-					typ: 111,
-					id,
-					..
-				} if text.starts_with("waiting for lock on ") => {
-					let mut drv = text.strip_prefix("waiting for lock on ").unwrap();
-					if let Some(txt) = drv.strip_prefix("\u{1b}[35;1m'") {
-						drv = txt;
-					}
-					if let Some(txt) = drv.strip_suffix("'\u{1b}[0m") {
-						drv = txt;
-					}
-					if let Some(txt) = drv.split("', '").next() {
-						drv = txt;
-					}
-					if let Some(pkg) = drv.strip_prefix("/nix/store/") {
-						let mut it = pkg.splitn(2, '-');
-						it.next();
-						if let Some(pkg) = it.next() {
-							drv = pkg;
-						}
-					}
-					let span = info_span!("waiting on drv", drv);
-					#[cfg(feature = "indicatif")]
-					span.pb_start();
-					self.spans.insert(id, span);
-					// Concurrent build of the same message
-				}
-				NixLog::Stop { id, .. } => {
-					self.spans.remove(&id);
-				}
-				NixLog::Result { fields, id, typ } if typ == 101 && !fields.is_empty() => {
-					if let Some(span) = self.spans.get(&id) {
-						if let LogField::String(s) = &fields[0] {
-							#[cfg(feature = "indicatif")]
-							span.pb_set_message(&process_message(s.trim()));
-							#[cfg(not(feature = "indicatif"))]
-							{
-								let _span = span.enter();
-								info!("{}", process_message(s));
-							}
-						} else {
-							warn!("bad fields: {fields:?}");
-						}
-					} else {
-						warn!("unknown result id: {id} {typ} {fields:?}");
-					}
-					// dbg!(fields, id, typ);
-				}
-				NixLog::Result { fields, id, typ } if typ == 105 && fields.len() >= 4 => {
-					if let Some(span) = self.spans.get(&id) {
-						if let [
-							LogField::Num(done),
-							LogField::Num(expected),
-							LogField::Num(_running),
-							LogField::Num(_failed),
-						] = &fields[..4]
-						{
-							#[cfg(feature = "indicatif")]
-							{
-								span.pb_set_length(*expected);
-								span.pb_set_position(*done);
-							}
-							let _ = (span, done, expected);
-						} else {
-							warn!("bad fields: {fields:?}");
-						}
-					} else {
-						// warn!("unknown result id: {id} {typ} {fields:?}");
-						// Unaccounted progress.
-					}
-					// dbg!(fields, id, typ);
-				}
-				NixLog::Result { typ, .. } if typ == 104 || typ == 106 => {
-					// Set phase, expected
-				}
-				_ => warn!("unknown log: {:?}", log),
-			};
-		} else {
-			let e = e.trim();
-			if e.starts_with("Failed tcsetattr(TCSADRAIN): ") {
-				return;
-			}
-			info!("{e}")
-		}
-	}
-}
deletedcrates/better-command/src/lib.rsdiffbeforeafterboth
--- a/crates/better-command/src/lib.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-mod handler;
-pub use handler::{ClonableHandler, Handler, NixHandler, NoopHandler, PlainHandler};
modifiedcrates/fleet-base/Cargo.tomldiffbeforeafterboth
--- a/crates/fleet-base/Cargo.toml
+++ b/crates/fleet-base/Cargo.toml
@@ -5,9 +5,15 @@
 rust-version.workspace = true
 
 [dependencies]
+remowt-client.workspace = true
+remowt-endpoints.workspace = true
+remowt-fleet.workspace = true
+remowt-link-shared.workspace = true
+remowt-ui-prompt.workspace = true
+
 age.workspace = true
 anyhow.workspace = true
-better-command.workspace = true
+camino.workspace = true
 chrono.workspace = true
 clap = { workspace = true, features = ["derive"] }
 fleet-shared.workspace = true
@@ -18,15 +24,11 @@
 nix-eval.workspace = true
 nixlike.workspace = true
 nom.workspace = true
-openssh.workspace = true
 rand.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 tabled.workspace = true
 tempfile.workspace = true
-thiserror.workspace = true
 time = { workspace = true, features = ["parsing"] }
 tokio.workspace = true
-tokio-util = { workspace = true, features = ["codec"] }
-toml_edit.workspace = true
 tracing.workspace = true
deletedcrates/fleet-base/src/command.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/command.rs
+++ /dev/null
@@ -1,442 +0,0 @@
-use std::{ffi::OsStr, pin, process::Stdio, sync::Arc, task::Poll};
-
-use anyhow::{Result, anyhow};
-use better_command::{Handler, NixHandler, PlainHandler};
-use futures::StreamExt;
-use itertools::Either;
-use openssh::{OverSsh, OwningCommand, Session};
-use serde::de::DeserializeOwned;
-use tokio::{io::AsyncRead, process::Command, select};
-use tokio_util::codec::{BytesCodec, FramedRead, LinesCodec};
-use tracing::debug;
-
-use crate::host::EscalationStrategy;
-
-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('\'');
-}
-fn ostoutf8(os: impl AsRef<OsStr>) -> String {
-	os.as_ref().to_str().expect("non-utf8 data").to_owned()
-}
-
-#[derive(Clone, Debug)]
-pub struct MyCommand {
-	command: String,
-	args: Vec<String>,
-	env: Vec<(String, String)>,
-	ssh_session: Option<Arc<Session>>,
-	escalation: EscalationStrategy,
-	escalate: bool,
-}
-impl MyCommand {
-	pub fn new_on(
-		escalation: EscalationStrategy,
-		cmd: impl AsRef<OsStr>,
-		session: Arc<Session>,
-	) -> Self {
-		assert!(!cmd.as_ref().is_empty());
-		Self {
-			command: ostoutf8(cmd),
-			args: vec![],
-			env: vec![],
-			ssh_session: Some(session),
-			escalation,
-			escalate: false,
-		}
-	}
-	pub fn new(escalation: EscalationStrategy, cmd: impl AsRef<OsStr>) -> Self {
-		assert!(!cmd.as_ref().is_empty());
-		Self {
-			command: ostoutf8(cmd),
-			args: vec![],
-			env: vec![],
-			ssh_session: None,
-			escalation,
-			escalate: false,
-		}
-	}
-	fn new_here(&self, cmd: impl AsRef<OsStr>) -> Self {
-		match self.ssh_session.clone() {
-			Some(ssh_session) => Self::new_on(self.escalation, cmd, ssh_session),
-			_ => Self::new(self.escalation, cmd),
-		}
-	}
-
-	fn into_args(self) -> Vec<String> {
-		let mut out = Vec::new();
-		if !self.env.is_empty() {
-			out.push("env".to_owned());
-			for (k, v) in self.env {
-				assert!(!k.contains('='));
-				out.push(format!("{k}={v}"));
-			}
-		}
-		out.push(self.command);
-		out.extend(self.args);
-		out
-	}
-
-	/// Translates environment variables into env command execution.
-	/// Required for ssh, as ssh don't allow to send environment variables (at least by default).
-	///
-	/// FIXME: Insecure, as arguments might be seen by other users on the same machine.
-	/// Figure out some way to transfer environment using stdio?
-	fn translate_env_into_env(self) -> Self {
-		if self.env.is_empty() {
-			return self;
-		}
-		let mut out = self.new_here("env");
-		for (k, v) in self.env {
-			assert!(!k.contains('='));
-			out.arg(format!("{k}={v}"));
-		}
-		out.arg(self.command);
-		out.args(self.args);
-
-		out
-	}
-	fn into_string(self) -> String {
-		let mut out = String::new();
-		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);
-			}
-		}
-		if !out.is_empty() {
-			out.push(' ');
-		}
-		escape_bash(&self.command, &mut out);
-		for arg in self.args {
-			out.push(' ');
-			escape_bash(&arg, &mut out);
-		}
-		out
-	}
-	fn into_command_unchecked_local(self) -> Command {
-		let mut out = Command::new(self.command);
-		out.args(self.args);
-		for (k, v) in self.env {
-			out.env(k, v);
-		}
-		out
-	}
-	fn into_command(self) -> Result<Either<Command, openssh::OwningCommand<Arc<Session>>>> {
-		Ok(match self.ssh_session.clone() {
-			Some(session) => {
-				let cmd = self.translate_env_into_env().into_command_unchecked_local();
-				Either::Right(
-					cmd.over_ssh(session)
-						.map_err(|e| anyhow!("ssh error: {e}"))?,
-				)
-			}
-			_ => {
-				let cmd = self.into_command_unchecked_local();
-				Either::Left(cmd)
-			}
-		})
-	}
-	pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
-		let arg = arg.as_ref();
-		self.args.push(ostoutf8(arg));
-		self
-	}
-	pub fn eqarg(&mut self, arg: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {
-		let arg = arg.as_ref();
-		let value = value.as_ref();
-		let arg = ostoutf8(arg);
-		let value = ostoutf8(value);
-		self.arg(format!("{arg}={value}"));
-		self
-	}
-	pub fn comparg(&mut self, arg: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {
-		self.arg(arg);
-		self.arg(value);
-		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 args<V: AsRef<OsStr>>(&mut self, args: impl IntoIterator<Item = V>) -> &mut Self {
-		for arg in args.into_iter() {
-			let arg = arg.as_ref();
-			self.args.push(ostoutf8(arg));
-		}
-		self
-	}
-	pub fn sudo(mut self) -> Self {
-		self.escalate = true;
-		self
-	}
-	fn wrap_sudo_if_needed(self) -> Self {
-		if !self.escalate {
-			return self;
-		}
-		match self.escalation {
-			EscalationStrategy::Su => {
-				let mut out = self.new_here("su");
-				out.arg("-c").arg(self.into_string());
-				out
-			}
-			EscalationStrategy::Sudo => {
-				let mut out = self.new_here("sudo");
-				out.args(self.into_args());
-				out
-			}
-			EscalationStrategy::Run0 => {
-				// run0 wants interactive authentication by default.
-				let mut run0 = self.new_here("run0");
-				let mut out = self.new_here("script");
-
-				// Red backgrounds messes with fleet formatting
-				run0.arg("--background=");
-				run0.args(self.into_args());
-
-				out.arg("-q");
-				out.arg("/dev/null");
-				out.arg("-c");
-				out.arg(run0.into_string());
-				dbg!(&out);
-				out
-			}
-		}
-	}
-
-	pub async fn run(self) -> Result<()> {
-		let str = self.clone().into_string();
-		let cmd = self.wrap_sudo_if_needed().into_command()?;
-		match cmd {
-			Either::Left(cmd) => run_nix_inner(str, cmd, &mut PlainHandler).await?,
-			Either::Right(cmd) => run_nix_inner_ssh(str, cmd, &mut PlainHandler).await?,
-		};
-		Ok(())
-	}
-	pub async fn run_string(self) -> Result<String> {
-		let bytes = self.run_bytes().await?;
-		Ok(String::from_utf8(bytes)?)
-	}
-	pub async fn run_value<T: DeserializeOwned>(self) -> Result<T> {
-		let v = self.run_string().await?;
-		Ok(serde_json::from_str(&v)?)
-	}
-	pub async fn run_bytes(self) -> Result<Vec<u8>> {
-		let str = self.clone().into_string();
-		let cmd = self.wrap_sudo_if_needed().into_command()?;
-		let v = match cmd {
-			Either::Left(cmd) => run_nix_inner_stdout(str, cmd, &mut PlainHandler).await?,
-			Either::Right(cmd) => run_nix_inner_stdout_ssh(str, cmd, &mut PlainHandler).await?,
-		};
-		Ok(v)
-	}
-
-	pub async fn run_nix_string(mut self) -> Result<String> {
-		let str = self.clone().into_string();
-		self.arg("--log-format").arg("internal-json");
-		let cmd = self.wrap_sudo_if_needed().into_command()?;
-		let bytes = match cmd {
-			Either::Left(cmd) => run_nix_inner_stdout(str, cmd, &mut NixHandler::default()).await?,
-			Either::Right(cmd) => {
-				run_nix_inner_stdout_ssh(str, cmd, &mut NixHandler::default()).await?
-			}
-		};
-		Ok(String::from_utf8(bytes)?)
-	}
-	pub async fn run_nix(mut self) -> Result<()> {
-		let str = self.clone().into_string();
-		self.arg("--log-format").arg("internal-json");
-		let cmd = self.wrap_sudo_if_needed().into_command()?;
-		match cmd {
-			Either::Left(mut cmd) => {
-				cmd.stdout(Stdio::inherit());
-				run_nix_inner(str, cmd, &mut NixHandler::default()).await
-			}
-			Either::Right(mut cmd) => {
-				cmd.stdout(openssh::Stdio::inherit());
-				run_nix_inner_ssh(str, cmd, &mut NixHandler::default()).await
-			}
-		}
-	}
-}
-
-struct EmptyAsyncRead;
-impl AsyncRead for EmptyAsyncRead {
-	fn poll_read(
-		self: std::pin::Pin<&mut Self>,
-		_cx: &mut std::task::Context<'_>,
-		_buf: &mut tokio::io::ReadBuf<'_>,
-	) -> Poll<std::io::Result<()>> {
-		Poll::Pending
-	}
-}
-
-async fn run_nix_inner_stdout(
-	str: String,
-	cmd: Command,
-	handler: &mut dyn Handler,
-) -> Result<Vec<u8>> {
-	Ok(run_nix_inner_raw(str, cmd, true, handler, None)
-		.await?
-		.expect("has out"))
-}
-async fn run_nix_inner(str: String, cmd: Command, handler: &mut dyn Handler) -> Result<()> {
-	let v = run_nix_inner_raw(str, cmd, false, handler, None).await?;
-	assert!(v.is_none());
-	Ok(())
-}
-async fn run_nix_inner_stdout_ssh(
-	str: String,
-	cmd: OwningCommand<Arc<Session>>,
-	handler: &mut dyn Handler,
-) -> Result<Vec<u8>> {
-	Ok(run_nix_inner_raw_ssh(str, cmd, true, handler, None)
-		.await?
-		.expect("has out"))
-}
-async fn run_nix_inner_ssh(
-	str: String,
-	cmd: OwningCommand<Arc<Session>>,
-	handler: &mut dyn Handler,
-) -> Result<()> {
-	let v = run_nix_inner_raw_ssh(str, cmd, false, handler, None).await?;
-	assert!(v.is_none());
-	Ok(())
-}
-
-async fn run_nix_inner_raw(
-	str: String,
-	mut cmd: Command,
-	want_stdout: bool,
-	err_handler: &mut dyn Handler,
-	mut out_handler: Option<&mut dyn Handler>,
-) -> Result<Option<Vec<u8>>> {
-	cmd.stderr(Stdio::piped());
-	cmd.stdout(Stdio::piped());
-	debug!("running command {str:?} on local");
-	let mut child = cmd.spawn()?;
-	let mut stderr = child.stderr.take().unwrap();
-	let stdout = child.stdout.take().unwrap();
-	let mut err = FramedRead::new(&mut stderr, LinesCodec::new());
-	let mut out: Option<Box<dyn AsyncRead + Unpin + Send>> = Some(Box::new(stdout));
-	let mut ob = want_stdout
-		.then(|| out.take().unwrap())
-		.unwrap_or_else(|| Box::new(EmptyAsyncRead));
-	let mut ol = (!want_stdout)
-		.then(|| out.take().unwrap())
-		.unwrap_or_else(|| Box::new(EmptyAsyncRead));
-	let mut ob = FramedRead::new(&mut ob, BytesCodec::new());
-	let mut ol = FramedRead::new(&mut ol, LinesCodec::new());
-
-	// while let Some(line) = read.next().await? {}
-
-	let mut out_buf = if want_stdout { Some(vec![]) } else { None };
-	loop {
-		select! {
-			biased;
-
-			Some(e) = err.next() => {
-				let e = e?;
-				err_handler.handle_line(&e);
-			},
-			Some(o) = ob.next() => {
-				out_buf.as_mut().expect("stdout == wants_stdout").extend_from_slice(&o?);
-			},
-			Some(o) = ol.next() => {
-				let o = o?;
-				if let Some(out) = out_handler.as_mut() {
-					out.handle_line(&o)
-				} else {
-					err_handler.handle_line(&o)
-				}
-				// out_handler.handle_info(&o);
-			},
-			code = child.wait() => {
-				let code = code?;
-				if !code.success() {
-					anyhow::bail!("command '{str}' failed with status {}", code);
-				}
-				break;
-			}
-		}
-	}
-
-	Ok(out_buf)
-}
-async fn run_nix_inner_raw_ssh(
-	str: String,
-	mut cmd: OwningCommand<Arc<Session>>,
-	want_stdout: bool,
-	err_handler: &mut dyn Handler,
-	mut out_handler: Option<&mut dyn Handler>,
-) -> Result<Option<Vec<u8>>> {
-	debug!("running command {str:?} over ssh");
-	cmd.stderr(openssh::Stdio::piped());
-	cmd.stdout(openssh::Stdio::piped());
-	let mut child = cmd.spawn().await?;
-	let mut stderr = child.stderr().take().unwrap();
-	let stdout = child.stdout().take().unwrap();
-	let mut err = FramedRead::new(&mut stderr, LinesCodec::new());
-	let mut out: Option<Box<dyn AsyncRead + Unpin + Send>> = Some(Box::new(stdout));
-	let mut ob = want_stdout
-		.then(|| out.take().unwrap())
-		.unwrap_or_else(|| Box::new(EmptyAsyncRead));
-	let mut ol = (!want_stdout)
-		.then(|| out.take().unwrap())
-		.unwrap_or_else(|| Box::new(EmptyAsyncRead));
-	let mut ob = FramedRead::new(&mut ob, BytesCodec::new());
-	let mut ol = FramedRead::new(&mut ol, LinesCodec::new());
-
-	// while let Some(line) = read.next().await? {}
-
-	let mut out_buf = if want_stdout { Some(vec![]) } else { None };
-
-	let mut wait_future = pin::pin!(child.wait());
-	loop {
-		select! {
-			biased;
-
-			Some(e) = err.next() => {
-				let e = e?;
-				err_handler.handle_line(&e);
-			},
-			Some(o) = ob.next() => {
-				out_buf.as_mut().expect("stdout == wants_stdout").extend_from_slice(&o?);
-			},
-			Some(o) = ol.next() => {
-				let o = o?;
-				if let Some(out) = out_handler.as_mut() {
-					out.handle_line(&o)
-				} else {
-					err_handler.handle_line(&o)
-				}
-				// out_handler.handle_info(&o);
-			},
-			code = &mut wait_future => {
-				let code = code?;
-				if !code.success() {
-					anyhow::bail!("command '{str}' failed with status {}", code);
-				}
-				break;
-			}
-		}
-	}
-
-	Ok(out_buf)
-}
modifiedcrates/fleet-base/src/deploy.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/deploy.rs
+++ b/crates/fleet-base/src/deploy.rs
@@ -1,8 +1,13 @@
-use std::{path::PathBuf, time::Duration};
+use std::time::Duration;
 
 use anyhow::{Context as _, Result, anyhow, bail};
+use camino::Utf8PathBuf;
 use clap::ValueEnum;
 use itertools::Itertools;
+use remowt_endpoints::fs::FsClient;
+use remowt_endpoints::systemd::SystemdClient;
+use remowt_fleet::NixClient;
+use remowt_link_shared::BifConfig;
 use tokio::time::sleep;
 use tracing::{Instrument as _, error, info, info_span, warn};
 
@@ -59,10 +64,11 @@
 pub async fn deploy_task(
 	action: DeployAction,
 	host: &ConfigHost,
-	built: PathBuf,
+	built: Utf8PathBuf,
 	specialisation: Option<String>,
 	disable_rollback: bool,
 ) -> Result<()> {
+	let remowt = host.remowt().await?;
 	let deploy_kind = host.deploy_kind().await?;
 	if (deploy_kind == DeployKind::NixosInstall || deploy_kind == DeployKind::NixosLustrate)
 		&& !matches!(action, DeployAction::Boot | DeployAction::Upload)
@@ -86,7 +92,7 @@
 			generation.id, generation.datetime
 		);
 		{
-			let mut cmd = host.cmd("sh").await?;
+			let mut cmd = remowt.cmd("sh");
 			cmd.arg("-c").arg(format!("mark=$(mktemp -p /etc -t fleet_rollback_marker.XXXXX) && echo -n {} > $mark && mv --no-clobber $mark /etc/fleet_rollback_marker", generation.id));
 			if let Err(e) = cmd.sudo().run().await {
 				error!("failed to set rollback marker: {e}");
@@ -104,7 +110,7 @@
 		// if we fail to perform generation switch in time, then we will still call the activation script, and this may break something.
 		// Anyway, reboot will still help in this case.
 		if action.should_schedule_rollback_run() {
-			let mut cmd = host.cmd("systemd-run").await?;
+			let mut cmd = remowt.cmd("systemd-run");
 			cmd.comparg("--on-active", "3min")
 				.comparg("--unit", "rollback-watchdog-run")
 				.arg("systemctl")
@@ -116,14 +122,20 @@
 			}
 		}
 	}
+
+	let remowt = host.remowt().await?;
+	let fs = remowt.endpoints::<FsClient<_>>();
 	if deploy_kind == DeployKind::NixosLustrate {
 		// Fleet could also create this file, but as this operation is potentially disruptive,
 		// make user do it themself.
-		if !host.file_exists("/etc/NIXOS_LUSTRATE").await? {
+		if !fs
+			.file_exists(Utf8PathBuf::from("/etc/NIXOS_LUSTRATE"))
+			.await?
+		{
 			bail!("/etc/NIXOS_LUSTRATE should be created on remote host");
 		}
 		// Wanted by NixOS to recognize the system as NixOS.
-		let mut cmd = host.cmd("touch").await?;
+		let mut cmd = remowt.cmd("touch");
 		cmd.arg("/etc/NIXOS");
 		cmd.sudo().run().await.context("creating /etc/NIXOS")?;
 	}
@@ -131,7 +143,7 @@
 		info!(
 			"running nixos-install to switch profile, install bootloader, and perform activation"
 		);
-		let mut cmd = host.cmd("nixos-install").await?;
+		let mut cmd = remowt.cmd("nixos-install");
 		cmd.arg("--system").arg(&built).args([
 			// Channels here aren't fleet host system channels, but channels embedded in installation cd, which might be old.
 			// It is possible to copy host channels, but I would prefer non-flake nix just to be unsupported.
@@ -147,32 +159,38 @@
 		if action.should_switch_profile() && !failed {
 			info!("switching system profile generation");
 
-			// To avoid even more problems, using nixos-install for now.
-			// // nix build is unable to work with --store argument for some reason, and nix until 2.26 didn't support copy with --profile argument,
-			// // falling back to using nix-env command
-			// // After stable NixOS starts using 2.26 - use `nix --store /mnt copy --from /mnt --profile ...` here, and instead of nix build below.
-			// let mut cmd = host.cmd("nix-env").await?;
-			// cmd.args([
-			// 	"--store",
-			// 	"/mnt",
-			// 	"--profile",
-			// 	"/mnt/nix/var/nix/profiles/system",
-			// 	"--set",
-			// ])
-			// .arg(&built);
-			// if let Err(e) = cmd.sudo().run_nix().await {
-			// 	error!("failed to switch system profile generation: {e}");
-			// 	failed = true;
-			// }
-			// It would also be possible to update profile atomically during copy:
-			// https://github.com/NixOS/nix/pull/11657
-			let mut cmd = host.nix_cmd().await?;
-			cmd.arg("build");
-			cmd.comparg("--profile", "/nix/var/nix/profiles/system");
-			cmd.arg(&built);
-			if let Err(e) = cmd.sudo().run_nix().await {
-				error!("failed to switch system profile generation: {e}");
-				failed = true;
+			match host.ensure_nix_plugin().await {
+				Ok(plugin_id) => {
+					let nix_elevated = remowt.plugin_endpoints::<NixClient<_>>(plugin_id);
+					// To avoid even more problems, using nixos-install for now.
+					// // nix build is unable to work with --store argument for some reason, and nix until 2.26 didn't support copy with --profile argument,
+					// // falling back to using nix-env command
+					// // After stable NixOS starts using 2.26 - use `nix --store /mnt copy --from /mnt --profile ...` here, and instead of nix build below.
+					// let mut cmd = host.cmd("nix-env").await?;
+					// cmd.args([
+					// 	"--store",
+					// 	"/mnt",
+					// 	"--profile",
+					// 	"/mnt/nix/var/nix/profiles/system",
+					// 	"--set",
+					// ])
+					// .arg(&built);
+					// if let Err(e) = cmd.sudo().run_nix().await {
+					// 	error!("failed to switch system profile generation: {e}");
+					// 	failed = true;
+					// }
+					if let Err(e) = nix_elevated
+						.switch_profile("/nix/var/nix/profiles/system".to_owned(), built.clone())
+						.await
+					{
+						error!("failed to switch system profile generation: {e}");
+						failed = true;
+					}
+				}
+				Err(e) => {
+					failed = true;
+					error!("failed to enable nix plugin: {e:?}");
+				}
 			}
 		}
 
@@ -189,7 +207,7 @@
 				built.clone()
 			};
 			let switch_script = specialised.join("bin/switch-to-configuration");
-			let mut cmd = host.cmd("systemd-run").in_current_span().await?;
+			let mut cmd = remowt.cmd("systemd-run");
 			if deploy_kind == DeployKind::NixosLustrate {
 				cmd.arg("--setenv=NIXOS_INSTALL_BOOTLOADER=1");
 			}
@@ -209,12 +227,14 @@
 		}
 	}
 	if action.should_create_rollback_marker() {
+		let elevated_systemd = remowt.run0_endpoints::<SystemdClient<BifConfig>>().await?;
+		let elevated_fs = remowt.run0_endpoints::<FsClient<BifConfig>>().await?;
 		if !disable_rollback {
 			if failed {
 				if action.should_schedule_rollback_run() {
 					info!("executing rollback");
-					if let Err(e) = host
-						.systemctl_start("rollback-watchdog.service")
+					if let Err(e) = elevated_systemd
+						.start("rollback-watchdog.service".to_owned())
 						.instrument(info_span!("rollback"))
 						.await
 					{
@@ -223,8 +243,8 @@
 				}
 			} else {
 				info!("trying to mark upgrade as successful");
-				if let Err(e) = host
-					.rm_file("/etc/fleet_rollback_marker", true)
+				if let Err(e) = elevated_fs
+					.rm_file(Utf8PathBuf::from("/etc/fleet_rollback_marker"))
 					.in_current_span()
 					.await
 				{
@@ -234,16 +254,22 @@
 				}
 			}
 			info!("disarming watchdog, just in case");
-			if let Err(_e) = host.systemctl_stop("rollback-watchdog.timer").await {
+			if let Err(_e) = elevated_systemd
+				.stop("rollback-watchdog.timer".to_owned())
+				.await
+			{
 				// It is ok, if there was no reboot - then timer might not be running.
 			}
 			if action.should_schedule_rollback_run() {
-				if let Err(e) = host.systemctl_stop("rollback-watchdog-run.timer").await {
+				if let Err(e) = elevated_systemd
+					.stop("rollback-watchdog-run.timer".to_owned())
+					.await
+				{
 					error!("failed to disarm rollback run: {e}");
 				}
 			}
-		} else if let Err(_e) = host
-			.rm_file("/etc/fleet_rollback_marker", true)
+		} else if let Err(_e) = elevated_fs
+			.rm_file(Utf8PathBuf::from("/etc/fleet_rollback_marker"))
 			.in_current_span()
 			.await
 		{
@@ -257,8 +283,8 @@
 	config: &Config,
 	host: &ConfigHost,
 	location: GenerationStorage,
-	generation: PathBuf,
-) -> Result<PathBuf> {
+	generation: Utf8PathBuf,
+) -> Result<Utf8PathBuf> {
 	if matches!(location, GenerationStorage::Pusher) {
 		bail!("pusher is not enabled in this version of fleet");
 	}
@@ -273,11 +299,11 @@
 				}
 				Err(e) if tries < 3 => {
 					tries += 1;
-					warn!("copy failure ({}/3): {}", tries, e);
+					warn!("copy failure ({}/3): {:#}", tries, e);
 					sleep(Duration::from_millis(5000)).await;
 				}
 				Err(e) => {
-					bail!("upload failed: {e}");
+					bail!("upload failed: {e:#}");
 				}
 			}
 		}
modifiedcrates/fleet-base/src/fleetdata.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/fleetdata.rs
+++ b/crates/fleet-base/src/fleetdata.rs
@@ -144,10 +144,6 @@
 	pub generation_data: Value,
 }
 
-fn is_false(b: &bool) -> bool {
-	*b == false
-}
-
 #[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
 #[repr(transparent)]
 pub struct SecretOwner(String);
@@ -291,7 +287,7 @@
 	}
 
 	// Which one is easier to access
-	return a.owners.len().cmp(&b.owners.len());
+	a.owners.len().cmp(&b.owners.len())
 }
 
 impl FleetSecretDistributions {
modifiedcrates/fleet-base/src/host.rsdiffbeforeafterboth
before · crates/fleet-base/src/host.rs
1use std::{2	collections::{BTreeMap, BTreeSet, HashSet},3	ffi::{OsStr, OsString},4	fmt::Display,5	io::Write,6	ops::Deref,7	path::PathBuf,8	str::FromStr,9	sync::{Arc, Mutex, MutexGuard, OnceLock},10};1112use anyhow::{Context, Result, anyhow, bail, ensure};13use chrono::{DateTime, Utc};14use fleet_shared::SecretData;15use nix_eval::{Value, nix_go, nix_go_json, util::assert_warn};16use openssh::{ControlPersist, SessionBuilder};17use serde::de::DeserializeOwned;18use tabled::Tabled;19use tempfile::NamedTempFile;20use time::{UtcDateTime, format_description};21use tracing::warn;2223use crate::{24	command::MyCommand,25	fleetdata::{26		FleetData, FleetSecretData, FleetSecretDistribution, FleetSecretPart, SecretOwner,27	},28};2930pub struct FleetConfigInternals {31	pub prefer_identities: BTreeSet<SecretOwner>,32	pub now: DateTime<Utc>,3334	/// Fleet project directory, containing fleet.nix file.35	pub directory: PathBuf,36	/// builtins.currentSystem37	pub local_system: String,38	pub data: Arc<FleetData>,39	pub nix_args: Vec<OsString>,40	/// fleet_config.config41	pub config_field: Value,42	/// flake.output43	pub flake_outputs: Value,44	// TODO: Remove with connectivity refactor45	pub localhost: String,4647	/// import nixpkgs {system = local};48	pub default_pkgs: Value,49	/// inputs.nixpkgs50	pub nixpkgs: Value,51}5253// TODO: Make field not pub54#[derive(Clone)]55pub struct Config(pub Arc<FleetConfigInternals>);5657impl Deref for Config {58	type Target = FleetConfigInternals;5960	fn deref(&self) -> &Self::Target {61		&self.062	}63}6465#[derive(Clone, Copy, Debug)]66pub enum EscalationStrategy {67	Sudo,68	Run0,69	Su,70}7172#[derive(Clone, PartialEq, Copy, Debug)]73pub enum DeployKind {74	/// NixOS => NixOS managed by fleet75	UpgradeToFleet,76	/// NixOS managed by fleet => NixOS managed by fleet77	Fleet,78	/// Remote host has /mnt, /mnt/boot mounted,79	/// generated config is added to fleet configuration.80	NixosInstall,81	/// Remote host has some system and nix installed in multi-user mode (/nix is owned by root),82	/// generated config is added to fleet configuration,83	/// and /etc/NIXOS_LUSTRATE exists, fleet will perform the rest.84	NixosLustrate,85}8687impl FromStr for DeployKind {88	type Err = anyhow::Error;89	fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {90		match s {91			"upgrade-to-fleet" => Ok(Self::UpgradeToFleet),92			"fleet" => Ok(Self::Fleet),93			"nixos-install" => Ok(Self::NixosInstall),94			"nixos-lustrate" => Ok(Self::NixosLustrate),95			v => bail!(96				"unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\", \"nixos-install\", \"nixos-lustrate\""97			),98		}99	}100}101pub struct ConfigHost {102	config: Config,103	pub name: String,104	groups: OnceLock<Vec<String>>,105106	// TODO: Both of those values are taken from host opts, there should be a cleaner way to specify it107	deploy_kind: OnceLock<DeployKind>,108	session_destination: OnceLock<String>,109	legacy_ssh_store: OnceLock<bool>,110111	pub host_config: Option<Value>,112	pub nixos_config: OnceLock<Value>,113	pub nixos_unchecked_config: OnceLock<Value>,114	pub pkgs_override: Option<Value>,115116	// TODO: Move command helpers away with connectivity refactor117	pub local: bool,118	pub session: OnceLock<Arc<openssh::Session>>,119}120121#[derive(Debug, Clone, Copy)]122pub enum GenerationStorage {123	Deployer,124	Machine,125	Pusher,126}127impl GenerationStorage {128	fn prefix(&self) -> &'static str {129		match self {130			GenerationStorage::Deployer => "deployer.",131			GenerationStorage::Machine => "",132			GenerationStorage::Pusher => "pusher.",133		}134	}135}136137#[derive(Tabled, Debug)]138pub struct Generation {139	#[tabled(rename = "ID", format("{}", self.rollback_id()))]140	pub id: u32,141	#[tabled(rename = "Current")]142	pub current: bool,143	#[tabled(rename = "Created at")]144	pub datetime: UtcDateTime,145	#[tabled(format = "{:?}")]146	pub store_path: PathBuf,147	#[tabled(skip)]148	pub location: GenerationStorage,149}150impl Generation {151	pub fn rollback_id(&self) -> String {152		format!("{}{}", self.location.prefix(), self.id)153	}154}155156fn parse_generation_line(g: &str) -> Option<Generation> {157	let mut parts = g.split_whitespace();158	let id = parts.next()?;159	let id: u32 = id.parse().ok()?;160	let date = parts.next()?;161	let time = parts.next()?;162	let current = if let Some(current) = parts.next() {163		if current == "(current)" {164			Some(true)165		} else {166			None167		}168	} else {169		Some(false)170	};171	let current = current?;172	if parts.next().is_some() {173		warn!("unexpected text after generation: {g}");174	}175176	let format = format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]")177		.expect("valid format");178	let datetime = UtcDateTime::parse(&format!("{date} {time}"), &format).ok()?;179180	Some(Generation {181		id,182		current,183		datetime,184		store_path: PathBuf::new(),185		location: GenerationStorage::Machine,186	})187}188// TODO: Move command helpers away with connectivity refactor189impl ConfigHost {190	pub async fn list_generations(&self, profile: &str) -> Result<Vec<Generation>> {191		let mut cmd = self.cmd("nix-env").await?;192		cmd.comparg("--profile", format!("/nix/var/nix/profiles/{profile}"))193			.arg("--list-generations")194			.env("TZ", "UTC");195		// Sudo is required because --list-generations tries to acquire profile lock196		let data = cmd.sudo().run_string().await?;197		let mut generations = data198			.split('\n')199			.map(|e| e.trim())200			.filter(|&l| !l.is_empty())201			.filter_map(|g| {202				let generation = parse_generation_line(g);203				if generation.is_none() {204					warn!("bad generation: {g}");205				};206				generation207			})208			.collect::<Vec<_>>();209		for ele in generations.iter_mut() {210			let mut cmd = self.cmd("readlink").await?;211			cmd.arg("--")212				.arg(format!("/nix/var/nix/profiles/{profile}-{}-link", ele.id));213			let path = cmd.run_string().await?;214			ele.store_path = PathBuf::from(path.trim_end_matches("\n"));215		}216217		Ok(generations)218	}219220	pub fn set_session_destination(&self, dest: String) {221		self.session_destination222			.set(dest)223			.expect("session destination is already set")224	}225	pub fn set_deploy_kind(&self, kind: DeployKind) {226		self.deploy_kind227			.set(kind)228			.expect("deploy kind is already set");229	}230	pub fn set_legacy_ssh_store(&self, legacy: bool) {231		self.legacy_ssh_store232			.set(legacy)233			.expect("legacy ssh store is already set")234	}235	pub async fn deploy_kind(&self) -> Result<DeployKind> {236		if let Some(kind) = self.deploy_kind.get() {237			return Ok(*kind);238		}239		let is_fleet_managed = match self.file_exists("/etc/FLEET_HOST").await {240			Ok(v) => v,241			Err(e) => {242				bail!("failed to query remote system kind: {e}");243			}244		};245		if !is_fleet_managed {246			bail!(247				"{}",248				indoc::indoc! {"249				host is not marked as managed by fleet250				if you're not trying to lustrate/install system from scratch,251				you should either252					1. manually create /etc/FLEET_HOST file on the target host,253					2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet254					3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos255			"}256			);257		}258		// TOCTOU is possible259		let _ = self.deploy_kind.set(DeployKind::Fleet);260		Ok(*self.deploy_kind.get().expect("deploy kind is just set"))261	}262	pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {263		// Prefer sudo, as run0 has some gotchas with polkit264		// and too many repeating prompts.265		if (self.find_in_path("sudo").await).is_ok() {266			return Ok(EscalationStrategy::Sudo);267		}268		if (self.find_in_path("run0").await).is_ok() {269			return Ok(EscalationStrategy::Run0);270		}271		Ok(EscalationStrategy::Su)272	}273	async fn open_session(&self) -> Result<Arc<openssh::Session>> {274		assert!(!self.local, "do not open ssh connection to local session");275		// FIXME: TOCTOU276		if let Some(session) = &self.session.get() {277			return Ok((*session).clone());278		};279		let mut session = SessionBuilder::default();280		session.control_persist(ControlPersist::ClosedAfterInitialConnection);281282		let dest = self.session_destination.get().unwrap_or(&self.name);283		let session = session284			.connect(&dest)285			.await286			.map_err(|e| anyhow!("ssh error while connecting to {}: {e:#?}", self.name))?;287		let session = Arc::new(session);288		self.session.set(session.clone()).expect("TOCTOU happened");289		Ok(session)290	}291	pub async fn mktemp_dir(&self) -> Result<String> {292		let mut cmd = self.cmd("mktemp").await?;293		cmd.arg("-d");294		let path = cmd.run_string().await?;295		Ok(path.trim_end().to_owned())296	}297	pub async fn file_exists(&self, path: impl AsRef<OsStr>) -> Result<bool> {298		let mut cmd = self.cmd("sh").await?;299		cmd.arg("-c")300			.arg("test -e \"$1\" && echo true || echo false")301			.arg("_")302			.arg(path);303		cmd.run_value().await304	}305	pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {306		let mut cmd = self.cmd("cat").await?;307		cmd.arg(path);308		cmd.run_bytes().await309	}310	pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {311		let mut cmd = self.cmd("cat").await?;312		cmd.arg(path);313		cmd.run_string().await314	}315	pub async fn read_dir(&self, path: impl AsRef<OsStr>) -> Result<Vec<String>> {316		let mut cmd = self.cmd("ls").await?;317		cmd.arg(path);318		let out = cmd.run_string().await?;319		let mut lines = out.split('\n');320		if let Some(last) = lines.next_back() {321			ensure!(last.is_empty(), "output of ls should end with newline");322		}323		Ok(lines.map(ToOwned::to_owned).collect())324	}325	#[allow(dead_code)]326	pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {327		let text = self.read_file_text(path).await?;328		Ok(serde_json::from_str(&text)?)329	}330	pub async fn read_env(&self, env: &str) -> Result<String> {331		let mut cmd = self.cmd("printenv").await?;332		cmd.arg(env);333		cmd.run_string().await334	}335	pub async fn find_in_path(&self, command: &str) -> Result<String> {336		// // `which` is not a part of coreutils, and it might not exist on machine.337		// let path = self.read_env("PATH").await?;338		// // Assuming delimiter is :, we don't work with windows host, this check will be much339		// // more sophisticated in remowt backend (and quicker, since actual PATH search will be done on remote machine)340		// for ele in path.split(':') {341		// 	let test_path = format!("{ele}/{cmd}");342		// 	test -x etc343		// }344		// let mut cmd = self.cmd("printenv").await?;345		// cmd.arg(env);346		// Ok(cmd.run_string().await?)347		// Assuming this is an environment issue if which doesn't exist, will be fixed with remowt.348		let mut cmd = self349			.cmd_escalation(350				// Not used351				EscalationStrategy::Su,352				"which",353			)354			.await?;355		cmd.arg(command);356		cmd.run_string().await357	}358	pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>359	where360		<D as FromStr>::Err: Display,361	{362		let text = self.read_file_text(path).await?;363		D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))364	}365	pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {366		self.cmd_escalation(self.escalation_strategy().await?, cmd)367			.await368	}369	pub async fn cmd_escalation(370		&self,371		escalation: EscalationStrategy,372		cmd: impl AsRef<OsStr>,373	) -> Result<MyCommand> {374		if self.local {375			Ok(MyCommand::new(escalation, cmd))376		} else {377			let session = self.open_session().await?;378			Ok(MyCommand::new_on(escalation, cmd, session))379		}380	}381	pub async fn nix_cmd(&self) -> Result<MyCommand> {382		let mut nix = self.cmd("nix").await?;383		nix.args([384			"--extra-experimental-features",385			"nix-command",386			"--extra-experimental-features",387			"flakes",388		]);389		Ok(nix)390	}391392	pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {393		ensure!(data.encrypted, "secret is not encrypted");394		let mut cmd = self.cmd("fleet-install-secrets").await?;395		cmd.arg("decrypt").eqarg("--secret", data.to_string());396		let encoded = cmd397			.sudo()398			.run_string()399			.await400			.context("failed to call remote host for decrypt")?;401		let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;402		ensure!(!data.encrypted, "secret came out encrypted");403		Ok(data.data)404	}405	pub async fn reencrypt_distribution(406		&self,407		data: &FleetSecretDistribution,408		targets: BTreeSet<SecretOwner>,409		now: DateTime<Utc>,410	) -> Result<FleetSecretDistribution> {411		let mut parts = BTreeMap::new();412		for (part_name, part) in &data.secret.parts {413			parts.insert(414				part_name.clone(),415				if part.raw.encrypted {416					FleetSecretPart {417						raw: self.reencrypt(part.raw.clone(), targets.clone()).await?,418					}419				} else {420					part.clone()421				},422			);423		}424		let secret = FleetSecretData {425			created_at: data.secret.created_at,426			expires_at: data.secret.expires_at,427			generation_data: data.secret.generation_data.clone(),428			parts,429		};430		Ok(FleetSecretDistribution::new(targets, secret, now))431	}432	pub async fn reencrypt(433		&self,434		data: SecretData,435		targets: BTreeSet<SecretOwner>,436	) -> Result<SecretData> {437		ensure!(data.encrypted, "secret is not encrypted");438		let mut cmd = self.cmd("fleet-install-secrets").await?;439		cmd.arg("reencrypt").eqarg("--secret", data.to_string());440		for target in targets {441			let key = self.config.key(&target).await?;442			cmd.eqarg("--targets", key);443		}444		let encoded = cmd445			.sudo()446			.run_string()447			.await448			.context("failed to call remote host for decrypt")?;449		let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;450		ensure!(data.encrypted, "secret came out not encrypted");451		Ok(data)452	}453	/// Returns path for futureproofing, as path might change i.e on conversion to CA454	pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {455		if self.local {456			// Path is located locally, thus already trusted.457			return Ok(path.to_owned());458		}459		let mut sign = MyCommand::new(460			// TODO: Look at the current escalation strategy.461			// ... or switch to run0 right after polkit update462			EscalationStrategy::Sudo,463			"nix",464		);465		sign.arg("store")466			.arg("sign")467			.comparg("--key-file", "/etc/nix/private-key")468			.arg("-r")469			.arg(&path);470		if let Err(e) = sign.sudo().run_nix().await {471			warn!("failed to sign store paths: {e}");472		}473		let mut nix = MyCommand::new(474			// Not used475			EscalationStrategy::Su,476			"nix",477		);478		nix.arg("copy").arg("--substitute-on-destination");479480		let proto = if self.legacy_ssh_store.get().cloned().unwrap_or(false) {481			"ssh"482		} else {483			"ssh-ng"484		};485486		match self.deploy_kind().await? {487			DeployKind::Fleet | DeployKind::UpgradeToFleet | DeployKind::NixosLustrate => {488				nix.comparg("--to", format!("{proto}://{}", self.name));489			}490			DeployKind::NixosInstall => {491				nix492					// Signature checking makes no sense with remote-store store argument set, as we're not even interacting with remote nix daemon493					.arg("--no-check-sigs")494					.comparg(495						"--to",496						format!("{proto}://root@{}?remote-store=/mnt", self.name),497					);498			}499		}500		nix.arg(path);501		nix.run_nix().await.context("nix copy")?;502		Ok(path.to_owned())503	}504	pub async fn systemctl_stop(&self, name: &str) -> Result<()> {505		let mut cmd = self.cmd("systemctl").await?;506		cmd.arg("stop").arg(name);507		cmd.sudo().run().await508	}509	pub async fn systemctl_start(&self, name: &str) -> Result<()> {510		let mut cmd = self.cmd("systemctl").await?;511		cmd.arg("start").arg(name);512		cmd.sudo().run().await513	}514515	pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {516		let mut cmd = self.cmd("rm").await?;517		cmd.arg("-f").arg(path);518		if sudo {519			cmd = cmd.sudo()520		}521		cmd.run().await522	}523}524525struct HostSecretDefinition(Value);526527impl ConfigHost {528	// TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,529	// assuming getting tags always returns the same value.530	pub fn tags(&self) -> Result<Vec<String>> {531		if let Some(v) = self.groups.get() {532			return Ok(v.clone());533		}534		let Some(host_config) = &self.host_config else {535			return Ok(vec![]);536		};537		let tags: Vec<String> = nix_go_json!(host_config.tags);538539		let _ = self.groups.set(tags.clone());540541		Ok(tags)542	}543	pub fn nixos_config(&self) -> Result<Value> {544		if let Some(v) = self.nixos_config.get() {545			return Ok(v.clone());546		}547		let Some(host_config) = &self.host_config else {548			bail!("local host has no nixos_config");549		};550		let nixos_config = nix_go!(host_config.nixos.config);551		assert_warn("nixos config evaluation", &nixos_config)?;552553		let _ = self.nixos_config.set(nixos_config.clone());554555		Ok(nixos_config)556	}557	pub fn nixos_unchecked_config(&self) -> Result<Value> {558		if let Some(v) = self.nixos_unchecked_config.get() {559			return Ok(v.clone());560		}561		let Some(host_config) = &self.host_config else {562			bail!("local host has no nixos_config");563		};564		let nixos_config = nix_go!(host_config.nixos_unchecked.config);565566		let _ = self.nixos_unchecked_config.set(nixos_config.clone());567568		Ok(nixos_config)569	}570571	pub fn list_defined_secrets(&self) -> Result<Vec<String>> {572		let nixos = self.nixos_unchecked_config()?;573		let secrets = nix_go!(nixos.secrets);574		secrets.list_fields()575	}576577	/// Packages for this host, resolved with nixpkgs overlays578	pub fn pkgs(&self) -> Result<Value> {579		if let Some(value) = &self.pkgs_override {580			return Ok(value.clone());581		}582		let Some(host_config) = &self.host_config else {583			bail!("local host has no host_config");584		};585		// TODO: Should nixos.options be cached?586		Ok(nix_go!(host_config.nixos.options._module.args.value.pkgs))587	}588}589590#[derive(Clone)]591pub struct SharedSecretDefinition(Value);592impl SharedSecretDefinition {593	pub fn expected_owners(&self) -> Result<BTreeSet<SecretOwner>> {594		let secret = &self.0;595		Ok(nix_go_json!(secret.expectedOwners))596	}597	pub fn allow_different(&self) -> Result<bool> {598		let secret = &self.0;599		Ok(nix_go_json!(secret.allowDifferent))600	}601	pub fn regenerate_on_owner_added(&self) -> Result<bool> {602		let secret = &self.0;603		Ok(nix_go_json!(secret.regenerateOnOwnerAdded))604	}605	pub fn regenerate_on_owner_removed(&self) -> Result<bool> {606		let secret = &self.0;607		Ok(nix_go_json!(secret.regenerateOnOwnerRemoved))608	}609	pub fn generator(&self) -> Result<Value> {610		let secret = &self.0;611		Ok(nix_go!(secret.generator))612	}613}614615impl Config {616	pub fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {617		let config = &self.config_field;618		let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);619		Ok(tagged)620	}621	pub fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {622		let mut out = BTreeSet::new();623		for owner in owners {624			if let Some(tag) = owner.strip_prefix('@') {625				let hosts = self.tagged_hostnames(tag)?;626				out.extend(hosts);627			} else {628				out.insert(owner);629			}630		}631		Ok(out)632	}633	pub fn local_host(&self) -> ConfigHost {634		ConfigHost {635			config: self.clone(),636			name: "<virtual localhost>".to_owned(),637			host_config: None,638			nixos_config: OnceLock::new(),639			nixos_unchecked_config: OnceLock::new(),640			groups: {641				let cell = OnceLock::new();642				let _ = cell.set(vec![]);643				cell644			},645			pkgs_override: Some(self.default_pkgs.clone()),646647			local: true,648			session: OnceLock::new(),649			deploy_kind: OnceLock::new(),650			session_destination: OnceLock::new(),651			legacy_ssh_store: OnceLock::new(),652		}653	}654655	pub fn preferred_hosts(656		&self,657		filter: impl Fn(&str) -> bool,658	) -> Result<impl Iterator<Item = Result<ConfigHost>>> {659		let prefer = self660			.prefer_identities661			.iter()662			.filter_map(|v| v.as_host())663			.collect::<HashSet<_>>();664		let config = &self.config_field;665		let mut names = nix_go!(config.hosts).list_fields()?;666		names.retain(|s| filter(s));667		names.sort_by_key(|h| prefer.contains(h.as_str()));668669		Ok(names.into_iter().map(|h| self.host(&h)))670	}671672	pub fn host(&self, name: &str) -> Result<ConfigHost> {673		let config = &self.config_field;674		let host_config = nix_go!(config.hosts[{ name }]);675676		Ok(ConfigHost {677			config: self.clone(),678			name: name.to_owned(),679			host_config: Some(host_config),680			nixos_config: OnceLock::new(),681			nixos_unchecked_config: OnceLock::new(),682			groups: OnceLock::new(),683			pkgs_override: None,684685			// TODO: Remove with connectivit refactor686			local: self.localhost == name,687			session: OnceLock::new(),688			deploy_kind: OnceLock::new(),689			session_destination: OnceLock::new(),690			legacy_ssh_store: OnceLock::new(),691		})692	}693	pub fn list_hosts(&self) -> Result<Vec<ConfigHost>> {694		let config = &self.config_field;695		let names = nix_go!(config.hosts).list_fields()?;696		let mut out = vec![];697		for name in names {698			out.push(self.host(&name)?);699		}700		Ok(out)701	}702	// TODO: Replace usages with .host().nixos_config703	pub fn system_config(&self, host: &str) -> Result<Value> {704		let fleet_field = &self.config_field;705		Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))706	}707708	pub fn secret_definition(&self, secret: &str) -> Result<Option<SharedSecretDefinition>> {709		let config = &self.config_field;710		let shared_secrets = nix_go!(config.secrets);711		if !shared_secrets.has_field(secret)? {712			return Ok(None);713		}714		Ok(Some(SharedSecretDefinition(nix_go!(715			shared_secrets[secret]716		))))717	}718719	pub fn save(&self) -> Result<()> {720		let mut tempfile = NamedTempFile::new_in(self.directory.clone()).context("failed to create updated version of fleet.nix in the same directory as original.\nDo you have write access to it? Access only to the fleet.nix won't be enough, the directory is used for atomic overwrite operation.\nIt is not recommended to use fleet by root anyway, move fleet project to your home directory.")?;721		let data = nixlike::serialize(&*self.data)?;722		tempfile.write_all(723			format!(724				"# This file contains fleet state and shouldn't be edited by hand\n\n{data}\n\n# vim: ts=2 et nowrap\n"725			)726			.as_bytes(),727		)?;728		let mut fleet_data_path = self.directory.clone();729		fleet_data_path.push("fleet.nix");730		tempfile.persist(fleet_data_path)?;731		Ok(())732	}733}
after · crates/fleet-base/src/host.rs
1use std::{2	collections::{BTreeMap, BTreeSet, HashSet},3	future::Future,4	io::Write,5	ops::Deref,6	path::PathBuf,7	pin::Pin,8	str::FromStr,9	sync::{Arc, OnceLock},10};1112use anyhow::{Context, Result, anyhow, bail, ensure};13use camino::{Utf8Path, Utf8PathBuf};14use chrono::{DateTime, Utc};15use fleet_shared::SecretData;16use nix_eval::{Store, Value, nix_go, nix_go_json, util::assert_warn};17use remowt_client::{AgentBundle, Remowt};18use remowt_endpoints::fs::FsClient;19use remowt_link_shared::Address;20use remowt_ui_prompt::auto::AutoPrompter;21use remowt_ui_prompt::bifrost::PromptEndpoints;22use remowt_ui_prompt::{PrependSourcePrompter, Source};23use tabled::Tabled;24use tempfile::NamedTempFile;25use time::UtcDateTime;26use tokio::task::spawn_blocking;27use tracing::{info, warn};2829use crate::fleetdata::{30	FleetData, FleetSecretData, FleetSecretDistribution, FleetSecretPart, SecretOwner,31};3233pub struct FleetConfigInternals {34	pub prefer_identities: BTreeSet<SecretOwner>,35	pub now: DateTime<Utc>,3637	/// Fleet project directory, containing fleet.nix file.38	pub directory: PathBuf,39	/// builtins.currentSystem40	pub local_system: String,41	pub data: Arc<FleetData>,42	/// fleet_config.config43	pub config_field: Value,44	/// flake.output45	pub flake_outputs: Value,46	// TODO: Remove with connectivity refactor47	pub localhost: String,4849	/// import nixpkgs {system = local};50	pub default_pkgs: Value,51	/// inputs.nixpkgs52	pub nixpkgs: Value,5354	pub local_host: OnceLock<Arc<ConfigHost>>,55}5657// TODO: Make field not pub58#[derive(Clone)]59pub struct Config(pub Arc<FleetConfigInternals>);6061impl Deref for Config {62	type Target = FleetConfigInternals;6364	fn deref(&self) -> &Self::Target {65		&self.066	}67}6869#[derive(Clone, PartialEq, Copy, Debug)]70pub enum DeployKind {71	/// NixOS => NixOS managed by fleet72	UpgradeToFleet,73	/// NixOS managed by fleet => NixOS managed by fleet74	Fleet,75	/// Remote host has /mnt, /mnt/boot mounted,76	/// generated config is added to fleet configuration.77	NixosInstall,78	/// Remote host has some system and nix installed in multi-user mode (/nix is owned by root),79	/// generated config is added to fleet configuration,80	/// and /etc/NIXOS_LUSTRATE exists, fleet will perform the rest.81	NixosLustrate,82}8384impl FromStr for DeployKind {85	type Err = anyhow::Error;86	fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {87		match s {88			"upgrade-to-fleet" => Ok(Self::UpgradeToFleet),89			"fleet" => Ok(Self::Fleet),90			"nixos-install" => Ok(Self::NixosInstall),91			"nixos-lustrate" => Ok(Self::NixosLustrate),92			v => bail!(93				"unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\", \"nixos-install\", \"nixos-lustrate\""94			),95		}96	}97}98pub struct ConfigHost {99	config: Config,100	pub name: String,101	groups: OnceLock<Vec<String>>,102103	// TODO: Both of those values are taken from host opts, there should be a cleaner way to specify it104	deploy_kind: OnceLock<DeployKind>,105	session_destination: OnceLock<String>,106	legacy_ssh_store: OnceLock<bool>,107108	pub host_config: Option<Value>,109	pub nixos_config: OnceLock<Value>,110	pub nixos_unchecked_config: OnceLock<Value>,111	pub pkgs_override: Option<Value>,112113	// TODO: Move command helpers away with connectivity refactor114	pub local: bool,115	pub remowt: OnceLock<Remowt>,116	nix_store: OnceLock<Arc<Store>>,117	nix_plugin: tokio::sync::OnceCell<()>,118}119120const NIX_PLUGIN_ID: u16 = 2;121122fn agents_dir() -> Result<PathBuf> {123	std::env::var_os("REMOWT_AGENTS_DIR")124		.map(PathBuf::from)125		.or_else(|| option_env!("REMOWT_AGENTS_DIR").map(PathBuf::from))126		.ok_or_else(|| {127			anyhow!("no remowt-agents bundle; set REMOWT_AGENTS_DIR to a remowt-agents output")128		})129}130131fn agent_bundle() -> Result<AgentBundle> {132	AgentBundle::from_dir(agents_dir()?)133}134135#[derive(Debug, Clone, Copy)]136pub enum GenerationStorage {137	Deployer,138	Machine,139	Pusher,140}141impl GenerationStorage {142	fn prefix(&self) -> &'static str {143		match self {144			GenerationStorage::Deployer => "deployer.",145			GenerationStorage::Machine => "",146			GenerationStorage::Pusher => "pusher.",147		}148	}149}150151#[derive(Tabled, Debug)]152pub struct Generation {153	#[tabled(rename = "ID", format("{}", self.rollback_id()))]154	pub id: u32,155	#[tabled(rename = "Current")]156	pub current: bool,157	#[tabled(rename = "Created at")]158	pub datetime: UtcDateTime,159	#[tabled(format = "{:?}")]160	pub store_path: Utf8PathBuf,161	#[tabled(skip)]162	pub location: GenerationStorage,163}164impl Generation {165	pub fn rollback_id(&self) -> String {166		format!("{}{}", self.location.prefix(), self.id)167	}168}169170impl ConfigHost {171	pub async fn list_generations(&self, profile: &str) -> Result<Vec<Generation>> {172		let plugin_id = self.ensure_nix_plugin().await?;173		let nix = self174			.remowt()175			.await?176			.plugin_endpoints::<remowt_fleet::NixClient<_>>(plugin_id);177		let raw = nix178			.list_generations(profile.to_owned())179			.await180			.map_err(|e| anyhow!("{e:?}"))?181			.map_err(|e| anyhow!("{e}"))?;182		raw.into_iter()183			.map(|g| {184				let id: u32 =185					g.id.try_into()186						.with_context(|| format!("generation id {} doesn't fit in u32", g.id))?;187				let datetime = UtcDateTime::from_unix_timestamp(g.creation_time_unix)188					.with_context(|| {189						format!("invalid generation timestamp {}", g.creation_time_unix)190					})?;191				Ok(Generation {192					id,193					current: g.current,194					datetime,195					store_path: g.store_path,196					location: GenerationStorage::Machine,197				})198			})199			.collect()200	}201202	pub fn set_session_destination(&self, dest: String) {203		self.session_destination204			.set(dest)205			.expect("session destination is already set")206	}207	pub fn set_deploy_kind(&self, kind: DeployKind) {208		self.deploy_kind209			.set(kind)210			.expect("deploy kind is already set");211	}212	pub fn set_legacy_ssh_store(&self, legacy: bool) {213		self.legacy_ssh_store214			.set(legacy)215			.expect("legacy ssh store is already set")216	}217	pub async fn deploy_kind(&self) -> Result<DeployKind> {218		if let Some(kind) = self.deploy_kind.get() {219			return Ok(*kind);220		}221		let remowt = self.remowt().await?;222		let fs = remowt.endpoints::<FsClient<_>>();223		let is_fleet_managed = match fs.file_exists(Utf8PathBuf::from("/etc/FLEET_HOST")).await {224			Ok(v) => v,225			Err(e) => {226				bail!("failed to query remote system kind: {e}");227			}228		};229		if !is_fleet_managed {230			bail!(231				"{}",232				indoc::indoc! {"233				host is not marked as managed by fleet234				if you're not trying to lustrate/install system from scratch,235				you should either236					1. manually create /etc/FLEET_HOST file on the target host,237					2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet238					3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos239				for installation use ?deploy_kind=nixos_install / ?deploy_kind=nixos_lustrate 240			"}241			);242		}243		// TOCTOU is possible244		let _ = self.deploy_kind.set(DeployKind::Fleet);245		Ok(*self.deploy_kind.get().expect("deploy kind is just set"))246	}247	async fn connection(&self) -> Result<Remowt> {248		if let Some(conn) = self.remowt.get() {249			return Ok(conn.clone());250		}251		let bundle = agent_bundle()?;252		let conn = if self.local {253			Remowt::connect_local(&bundle)254				.await255				.context("starting local remowt agent")?256		} else {257			let dest = self258				.session_destination259				.get()260				.cloned()261				.unwrap_or_else(|| self.name.clone());262			Remowt::connect(&dest, &bundle)263				.await264				.map_err(|e| anyhow!("remowt error while connecting to {}: {e:#?}", self.name))?265		};266		PromptEndpoints(PrependSourcePrompter {267			prompter: AutoPrompter::new().await,268			source: if self.local {269				vec![]270			} else {271				vec![Source(std::borrow::Cow::Owned(format!(272					"ssh host: {}",273					self.name274				)))]275			},276			description: "".to_owned(),277		})278		.register_endpoints(&mut conn.rpc());279		let _ = self.remowt.set(conn);280		Ok(self.remowt.get().expect("just set").clone())281	}282283	/// Client for this host's unprivileged agent.284	pub async fn remowt(&self) -> Result<Remowt> {285		Ok(self.connection().await?)286	}287288	pub fn ensure_nix_plugin(&self) -> Pin<Box<dyn Future<Output = Result<u16>> + Send + '_>> {289		Box::pin(async {290			self.nix_plugin291				.get_or_try_init(|| async {292					let pkgs = self.pkgs()?;293					let name = "remowt-plugin-fleet";294					let plugin = nix_go!(pkgs[{ name }]);295					let built = plugin296						.build("out")297						.context("failed to build the fleet nix plugin")?;298					let copied = self299						.remote_derivation(&built)300						.await301						.context("failed to copy the fleet nix plugin to the host store")?;302					let bin = copied.join("bin/remowt-plugin-fleet");303					self.remowt()304						.await?305						.run0_load_plugin_path(NIX_PLUGIN_ID, bin.as_str())306						.await307						.context("failed to load the fleet nix plugin")?;308					self.remowt()309						.await?310						.rpc()311						.wait_for_connection_to(Address::Plugin(NIX_PLUGIN_ID))312						.await313						.map_err(|e| anyhow!("failed to wait for plugin"))?;314					anyhow::Ok(())315				})316				.await?;317			Ok(NIX_PLUGIN_ID)318		})319	}320321	async fn nix_store(&self) -> Result<Arc<Store>> {322		if let Some(store) = self.nix_store.get() {323			return Ok(store.clone());324		}325		let conn = self.connection().await?;326		let socket = match self.deploy_kind().await? {327			DeployKind::NixosInstall => {328				remowt_fleet::nix_store_socket(conn, "/mnt?require-sigs=false").await?329			}330			_ => remowt_fleet::nix_store_socket(conn, "auto").await?,331		};332		let uri = format!("unix://{}", socket.display());333		let store = Arc::new(Store::open(&uri)?);334		let _ = self.nix_store.set(store);335		Ok(self.nix_store.get().expect("just set").clone())336	}337338	pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {339		ensure!(data.encrypted, "secret is not encrypted");340		let remowt = self.remowt().await?;341		let mut cmd = remowt.cmd("fleet-install-secrets");342		cmd.arg("decrypt").eqarg("--secret", data.to_string());343		let encoded = cmd344			.sudo()345			.run_string()346			.await347			.context("failed to call remote host for decrypt")?;348		let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;349		ensure!(!data.encrypted, "secret came out encrypted");350		Ok(data.data)351	}352	pub async fn reencrypt_distribution(353		&self,354		data: &FleetSecretDistribution,355		targets: BTreeSet<SecretOwner>,356		now: DateTime<Utc>,357	) -> Result<FleetSecretDistribution> {358		let mut parts = BTreeMap::new();359		for (part_name, part) in &data.secret.parts {360			parts.insert(361				part_name.clone(),362				if part.raw.encrypted {363					FleetSecretPart {364						raw: self.reencrypt(part.raw.clone(), targets.clone()).await?,365					}366				} else {367					part.clone()368				},369			);370		}371		let secret = FleetSecretData {372			created_at: data.secret.created_at,373			expires_at: data.secret.expires_at,374			generation_data: data.secret.generation_data.clone(),375			parts,376		};377		Ok(FleetSecretDistribution::new(targets, secret, now))378	}379	pub async fn reencrypt(380		&self,381		data: SecretData,382		targets: BTreeSet<SecretOwner>,383	) -> Result<SecretData> {384		let remowt = self.remowt().await?;385		ensure!(data.encrypted, "secret is not encrypted");386		let mut cmd = remowt.cmd("fleet-install-secrets");387		cmd.arg("reencrypt").eqarg("--secret", data.to_string());388		for target in targets {389			let key = self.config.key(&target).await?;390			cmd.eqarg("--targets", key);391		}392		let encoded = cmd393			.sudo()394			.run_string()395			.await396			.context("failed to call remote host for decrypt")?;397		let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;398		ensure!(data.encrypted, "secret came out not encrypted");399		Ok(data)400	}401	/// Returns path for futureproofing, as path might change i.e on conversion to CA402	pub async fn remote_derivation(&self, path: impl AsRef<Utf8Path>) -> Result<Utf8PathBuf> {403		let path = path.as_ref().to_owned();404		if self.local {405			// Path is located locally, thus already trusted.406			return Ok(path);407		}408		let sign: Pin<Box<dyn Future<Output = Result<()>> + Send>> = {409			let path = path.clone();410			Box::pin(async move {411				let local = self.config.local_host();412				let plugin_id = local.ensure_nix_plugin().await?;413				let nix = local414					.remowt()415					.await?416					.plugin_endpoints::<remowt_fleet::NixClient<_>>(plugin_id);417				nix.sign_closure(path, Utf8PathBuf::from("/etc/nix/private-key"))418					.await419					.map_err(|e| anyhow!("{e:?}"))?420					.map_err(|e| anyhow!("{e}"))?;421				Ok(())422			})423		};424		if let Err(e) = sign.await {425			warn!("failed to sign store paths: {e}");426		}427		let store = self.nix_store().await?;428		{429			let path = path.clone();430			spawn_blocking(move || nix_eval::copy_closure_to(&store, path.as_ref()))431				.await?432				.context("copying closure to remote store")?;433		}434		Ok(path)435	}436}437438struct HostSecretDefinition(Value);439440impl ConfigHost {441	// TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,442	// assuming getting tags always returns the same value.443	pub fn tags(&self) -> Result<Vec<String>> {444		if let Some(v) = self.groups.get() {445			return Ok(v.clone());446		}447		let Some(host_config) = &self.host_config else {448			return Ok(vec![]);449		};450		let tags: Vec<String> = nix_go_json!(host_config.tags);451452		let _ = self.groups.set(tags.clone());453454		Ok(tags)455	}456	pub fn nixos_config(&self) -> Result<Value> {457		if let Some(v) = self.nixos_config.get() {458			return Ok(v.clone());459		}460		let Some(host_config) = &self.host_config else {461			bail!("local host has no nixos_config");462		};463		let nixos_config = nix_go!(host_config.nixos.config);464		assert_warn("nixos config evaluation", &nixos_config)?;465466		let _ = self.nixos_config.set(nixos_config.clone());467468		Ok(nixos_config)469	}470	pub fn nixos_unchecked_config(&self) -> Result<Value> {471		if let Some(v) = self.nixos_unchecked_config.get() {472			return Ok(v.clone());473		}474		let Some(host_config) = &self.host_config else {475			bail!("local host has no nixos_config");476		};477		let nixos_config = nix_go!(host_config.nixos_unchecked.config);478479		let _ = self.nixos_unchecked_config.set(nixos_config.clone());480481		Ok(nixos_config)482	}483484	pub fn list_defined_secrets(&self) -> Result<Vec<String>> {485		let nixos = self.nixos_unchecked_config()?;486		let secrets = nix_go!(nixos.secrets);487		secrets.list_fields()488	}489490	/// Packages for this host, resolved with nixpkgs overlays491	pub fn pkgs(&self) -> Result<Value> {492		if let Some(value) = &self.pkgs_override {493			return Ok(value.clone());494		}495		let Some(host_config) = &self.host_config else {496			bail!("local host has no host_config");497		};498		// TODO: Should nixos.options be cached?499		Ok(nix_go!(host_config.nixos.options._module.args.value.pkgs))500	}501}502503#[derive(Clone)]504pub struct SharedSecretDefinition(Value);505impl SharedSecretDefinition {506	pub fn expected_owners(&self) -> Result<BTreeSet<SecretOwner>> {507		let secret = &self.0;508		Ok(nix_go_json!(secret.expectedOwners))509	}510	pub fn allow_different(&self) -> Result<bool> {511		let secret = &self.0;512		Ok(nix_go_json!(secret.allowDifferent))513	}514	pub fn regenerate_on_owner_added(&self) -> Result<bool> {515		let secret = &self.0;516		Ok(nix_go_json!(secret.regenerateOnOwnerAdded))517	}518	pub fn regenerate_on_owner_removed(&self) -> Result<bool> {519		let secret = &self.0;520		Ok(nix_go_json!(secret.regenerateOnOwnerRemoved))521	}522	pub fn generator(&self) -> Result<Value> {523		let secret = &self.0;524		Ok(nix_go!(secret.generator))525	}526}527528impl Config {529	pub fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {530		let config = &self.config_field;531		let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);532		Ok(tagged)533	}534	pub fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {535		let mut out = BTreeSet::new();536		for owner in owners {537			if let Some(tag) = owner.strip_prefix('@') {538				let hosts = self.tagged_hostnames(tag)?;539				out.extend(hosts);540			} else {541				out.insert(owner);542			}543		}544		Ok(out)545	}546	pub fn local_host(&self) -> Arc<ConfigHost> {547		self.local_host548			.get_or_init(|| {549				Arc::new(ConfigHost {550					config: self.clone(),551					name: "<virtual localhost>".to_owned(),552					host_config: None,553					nixos_config: OnceLock::new(),554					nixos_unchecked_config: OnceLock::new(),555					groups: {556						let cell = OnceLock::new();557						let _ = cell.set(vec![]);558						cell559					},560					pkgs_override: Some(self.default_pkgs.clone()),561562					local: true,563					remowt: OnceLock::new(),564					nix_store: OnceLock::new(),565					nix_plugin: tokio::sync::OnceCell::new(),566					deploy_kind: OnceLock::new(),567					session_destination: OnceLock::new(),568					legacy_ssh_store: OnceLock::new(),569				})570			})571			.clone()572	}573574	pub fn preferred_hosts(575		&self,576		filter: impl Fn(&str) -> bool,577	) -> Result<impl Iterator<Item = Result<ConfigHost>>> {578		let prefer = self579			.prefer_identities580			.iter()581			.filter_map(|v| v.as_host())582			.collect::<HashSet<_>>();583		let config = &self.config_field;584		let mut names = nix_go!(config.hosts).list_fields()?;585		names.retain(|s| filter(s));586		names.sort_by_key(|h| prefer.contains(h.as_str()));587588		Ok(names.into_iter().map(|h| self.host(&h)))589	}590591	pub fn host(&self, name: &str) -> Result<ConfigHost> {592		let config = &self.config_field;593		let host_config = nix_go!(config.hosts[{ name }]);594595		Ok(ConfigHost {596			config: self.clone(),597			name: name.to_owned(),598			host_config: Some(host_config),599			nixos_config: OnceLock::new(),600			nixos_unchecked_config: OnceLock::new(),601			groups: OnceLock::new(),602			pkgs_override: None,603604			// TODO: Remove with connectivit refactor605			local: self.localhost == name,606			remowt: OnceLock::new(),607			nix_store: OnceLock::new(),608			nix_plugin: tokio::sync::OnceCell::new(),609			deploy_kind: OnceLock::new(),610			session_destination: OnceLock::new(),611			legacy_ssh_store: OnceLock::new(),612		})613	}614	pub fn list_hosts(&self) -> Result<Vec<ConfigHost>> {615		let config = &self.config_field;616		let names = nix_go!(config.hosts).list_fields()?;617		let mut out = vec![];618		for name in names {619			out.push(self.host(&name)?);620		}621		Ok(out)622	}623	// TODO: Replace usages with .host().nixos_config624	pub fn system_config(&self, host: &str) -> Result<Value> {625		let fleet_field = &self.config_field;626		Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))627	}628629	pub fn secret_definition(&self, secret: &str) -> Result<Option<SharedSecretDefinition>> {630		let config = &self.config_field;631		let shared_secrets = nix_go!(config.secrets);632		if !shared_secrets.has_field(secret)? {633			return Ok(None);634		}635		Ok(Some(SharedSecretDefinition(nix_go!(636			shared_secrets[secret]637		))))638	}639640	pub fn save(&self) -> Result<()> {641		let mut tempfile = NamedTempFile::new_in(self.directory.clone()).context("failed to create updated version of fleet.nix in the same directory as original.\nDo you have write access to it? Access only to the fleet.nix won't be enough, the directory is used for atomic overwrite operation.\nIt is not recommended to use fleet by root anyway, move fleet project to your home directory.")?;642		let data = nixlike::serialize(&*self.data)?;643		tempfile.write_all(644			format!(645				"# This file contains fleet state and shouldn't be edited by hand\n\n{data}\n\n# vim: ts=2 et nowrap\n"646			)647			.as_bytes(),648		)?;649		let mut fleet_data_path = self.directory.clone();650		fleet_data_path.push("fleet.nix");651		tempfile.persist(fleet_data_path)?;652		Ok(())653	}654}
modifiedcrates/fleet-base/src/keys.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/keys.rs
+++ b/crates/fleet-base/src/keys.rs
@@ -31,7 +31,8 @@
 		} else {
 			warn!("Loading key for {}", host);
 			let host = self.host(host)?;
-			let mut cmd = host.cmd("cat").await?;
+			let remowt = host.remowt().await?;
+			let mut cmd = remowt.cmd("cat");
 			cmd.arg("/etc/ssh/ssh_host_ed25519_key.pub");
 			let key = cmd.run_string().await?;
 			self.update_key(&host.name, key.clone());
modifiedcrates/fleet-base/src/lib.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/lib.rs
+++ b/crates/fleet-base/src/lib.rs
@@ -1,8 +1,6 @@
-pub mod command;
 pub mod deploy;
 pub mod fleetdata;
 pub mod host;
 mod keys;
 pub mod opts;
 pub mod primops;
-pub mod secret_storage;
modifiedcrates/fleet-base/src/opts.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/opts.rs
+++ b/crates/fleet-base/src/opts.rs
@@ -1,9 +1,8 @@
 use std::{
 	collections::{BTreeMap, BTreeSet},
 	env::current_dir,
-	ffi::OsString,
 	str::FromStr,
-	sync::{Arc, Mutex},
+	sync::{Arc, OnceLock},
 };
 
 use anyhow::{Context, Result, bail};
@@ -196,7 +195,7 @@
 	}
 
 	// TODO: Config should be detached from opts.
-	pub fn build(&self, nix_args: Vec<OsString>, assert: bool) -> Result<Config> {
+	pub fn build(&self, assert: bool) -> Result<Config> {
 		let cwd = current_dir()?;
 		let mut directory = cwd.clone();
 		let mut fleet_data_path = directory.join("fleet.nix");
@@ -274,11 +273,11 @@
 			data,
 			flake_outputs: flake,
 			local_system: self.local_system.clone(),
-			nix_args,
 			config_field,
 			default_pkgs,
 			nixpkgs,
 			localhost: self.localhost.to_owned(),
+			local_host: OnceLock::new(),
 		}));
 
 		PRIMOPS_DATA
modifiedcrates/fleet-base/src/primops.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/primops.rs
+++ b/crates/fleet-base/src/primops.rs
@@ -1,10 +1,12 @@
 use std::collections::{BTreeMap, BTreeSet, HashMap};
-use std::sync::OnceLock;
+use std::sync::{Arc, OnceLock};
 
 use anyhow::{Context, bail, ensure};
 use fleet_shared::SecretData;
 use itertools::Itertools;
 use nix_eval::{NativeFn, Value, await_in_nix, nix_go, nix_go_json};
+use remowt_endpoints::fs::FsClient;
+use remowt_link_shared::BifConfig;
 use serde::Deserialize;
 use tracing::{info, warn};
 
@@ -28,7 +30,7 @@
 	let pkgs = host_on.pkgs()?;
 	let default_mk_secret_generators = nix_go!(pkgs.mkSecretGenerators);
 	let generators = nix_go!(default_mk_secret_generators(Obj { recipients }));
-	Ok(pkgs.clone().attrs_update(generators)?)
+	pkgs.clone().attrs_update(generators)
 }
 pub fn get_default_pkgs_and_generators(config: &Config) -> Result<Value> {
 	let host_on = config.local_host();
@@ -91,12 +93,18 @@
 			let impure_on: Option<String> = nix_go_json!(default_generator_drv.impureOn);
 
 			let host_on = if let Some(on) = &impure_on {
-				config
-					.host(on)
-					.context("failed to get secret generation target host")?
+				Arc::new(
+					config
+						.host(on)
+						.context("failed to get secret generation target host")?,
+				)
 			} else {
 				config.local_host()
 			};
+
+			let remowt = host_on.remowt().await?;
+			let fs = remowt.endpoints::<FsClient<BifConfig>>();
+
 			let mut recipients = Vec::new();
 			for owner in &expectations.owners {
 				recipients.push(config.key(owner).await?);
@@ -116,12 +124,13 @@
 				.context("failed to copy generator to target host")?;
 
 			// TODO: Remove destdir after everything is done
-			let out_parent = host_on
+			let out_parent = fs
 				.mktemp_dir()
 				.await
+				.map_err(|e| anyhow!("{e:?}"))
 				.context("failed to prepare generator output dir on target host")?;
-			let out = format!("{out_parent}/out");
-			let mut generator_cmd = host_on.cmd(generator).await?;
+			let out = out_parent.join("out");
+			let mut generator_cmd = remowt.cmd(generator);
 			generator_cmd.env("out", &out);
 			if impure_on.is_none() {
 				let project_path: String = config
@@ -138,7 +147,7 @@
 				.context("failed to run impure generator")?;
 
 			{
-				let marker = host_on.read_file_text(format!("{out}/marker")).await?;
+				let marker = fs.read_file_text(out.join("marker")).await?;
 				ensure!(
 					marker == "SUCCESS",
 					"impure generator ended prematurely, secret generation failed"
@@ -147,15 +156,16 @@
 
 			let mut missing_parts = expectations.parts.clone();
 			let mut parts = BTreeMap::new();
-			for part in host_on.read_dir(&out).await? {
+			for part in fs.read_dir(&out).await? {
+				let part = part.into_string();
 				if part == "created_at" || part == "expires_at" || part == "marker" {
 					continue;
 				}
 				let Some(part_def) = missing_parts.remove(&part) else {
 					bail!("secret generator has produced an unexpected part: {part}");
 				};
-				let contents: SecretData = host_on
-					.read_file_text(format!("{out}/{part}"))
+				let contents: SecretData = fs
+					.read_file_text(out.join("part"))
 					.await?
 					.parse()
 					.map_err(|e| anyhow!("failed to decode secret {out:?} part {part:?}: {e}"))?;
@@ -178,11 +188,12 @@
 				);
 			}
 
-			let created_at = host_on.read_file_value(format!("{out}/created_at")).await?;
-			let expires_at = host_on
-				.read_file_value(format!("{out}/expires_at"))
-				.await
-				.ok();
+			let created_at = fs.read_file_value(out.join("created_at")).await??;
+			let expires_at = match fs.read_file_value(out.join("expires_at")).await {
+				Ok(v) => Some(v?),
+				Err(remowt_endpoints::fs::Error::NotFound) => None,
+				Err(e) => return Err(e.into()),
+			};
 
 			let new_data = FleetSecretData {
 				created_at,
deletedcrates/fleet-base/src/secret_storage.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/secret_storage.rs
+++ /dev/null
@@ -1,278 +0,0 @@
-use anyhow::{Result, bail, ensure};
-use itertools::Itertools;
-use std::fs::{File, metadata};
-use std::io::{self, ErrorKind, Read, Write};
-use std::path::PathBuf;
-use std::str::FromStr;
-use std::{env, fs};
-
-use tempfile::{TempPath, tempfile_in};
-use toml_edit::{Document, DocumentMut, Formatted, Item, Value};
-
-struct Name(String);
-
-fn encode_name(name: &str) -> Name {
-	assert!(
-		!name.starts_with(['_', '.']),
-		"groups should not start with _ or ."
-	);
-	assert!(
-		!name.chars().any(|c| c == '/'),
-		"group name should not contain internal slash"
-	);
-	Name(name.to_owned())
-}
-
-enum RewriteError {
-	ConcurrentCreate,
-	ConcurrentDelete,
-	ConcurrentModify,
-	ConcurrentWrite,
-	Io(io::Error),
-	Persist(tempfile::PersistError),
-}
-
-fn safe_rewrite(
-	path: &PathBuf,
-	old_content: Option<Vec<u8>>,
-	new_content: Option<Vec<u8>>,
-) -> Result<(), RewriteError> {
-	let mut f = match (old_content.is_some(), new_content.is_some()) {
-		(false, true) => match File::create_new(path) {
-			Ok(v) => v,
-			Err(e) if e.kind() == ErrorKind::AlreadyExists => {
-				return Err(RewriteError::ConcurrentCreate);
-			}
-			Err(e) => return Err(RewriteError::Io(e)),
-		},
-		(true, _) => match File::open(&path) {
-			Ok(v) => v,
-			Err(_e) => return Err(RewriteError::ConcurrentDelete),
-		},
-		(false, false) => match metadata(&path) {
-			Err(e) if e.kind() == ErrorKind::NotFound => {
-				return Ok(());
-			}
-			Ok(_) => return Err(RewriteError::ConcurrentCreate),
-			Err(e) => return Err(RewriteError::Io(e)),
-		},
-	};
-	f.lock().map_err(RewriteError::Io)?;
-	let mut check_content = vec![];
-	f.read_to_end(&mut check_content)
-		.map_err(RewriteError::Io)?;
-	match &old_content {
-		Some(old) => {
-			if old != &check_content {
-				return Err(RewriteError::ConcurrentModify);
-			}
-		}
-		None => {
-			if !check_content.is_empty() {
-				return Err(RewriteError::ConcurrentDelete);
-			}
-		}
-	}
-	if let Some(new_content) = new_content {
-		if Some(&new_content) == old_content.as_ref() {
-			return Ok(());
-		}
-		let dir = path.parent().expect("file is in directory, thus not root");
-		let mut tempfile = tempfile::Builder::new()
-			.prefix(".rewrite-")
-			.tempfile_in(dir)
-			.map_err(RewriteError::Io)?;
-		tempfile.write_all(&new_content).map_err(RewriteError::Io)?;
-		tempfile.flush().map_err(RewriteError::Io)?;
-		tempfile.persist(path).map_err(RewriteError::Persist)?;
-	} else {
-		fs::remove_file(path).map_err(RewriteError::Io)?;
-	}
-	let _ = f.unlock();
-	Ok(())
-}
-fn update_string(path: PathBuf, modify: impl Fn(&mut Option<String>) -> Result<()>) -> Result<()> {
-	loop {
-		let orig = match fs::read_to_string(&path) {
-			Ok(v) => Some(v),
-			Err(e) if e.kind() == ErrorKind::NotFound => None,
-			Err(e) => return Err(e.into()),
-		};
-		let mut edit = orig.clone();
-		modify(&mut edit);
-
-		match safe_rewrite(&path, orig.map(String::into), edit.map(String::into)) {
-			Ok(()) => return Ok(()),
-			Err(
-				RewriteError::ConcurrentCreate
-				| RewriteError::ConcurrentModify
-				| RewriteError::ConcurrentWrite
-				| RewriteError::ConcurrentDelete,
-			) => {
-				continue;
-			}
-			Err(RewriteError::Io(io)) => return Err(io.into()),
-			Err(RewriteError::Persist(io)) => return Err(io.into()),
-		}
-	}
-}
-fn update_toml(path: PathBuf, modify: impl Fn(&mut DocumentMut) -> Result<()>) -> Result<()> {
-	update_string(path, |str| {
-		let mut doc = match str {
-			None => DocumentMut::new(),
-			Some(v) => DocumentMut::from_str(v)?,
-		};
-		modify(&mut doc)?;
-		if doc.is_empty() {
-			*str = None
-		} else {
-			*str = Some(doc.to_string())
-		}
-		Ok(())
-	})
-}
-fn update_lines(path: PathBuf, modify: impl Fn(&mut Vec<String>) -> Result<()>) -> Result<()> {
-	update_string(path, |str| {
-		let mut list = if let Some(str) = str {
-			str.split('\n').map(|s| s.to_owned()).collect_vec()
-		} else {
-			vec![]
-		};
-		let had_end_newline = if list.last().map(|v| v.as_str()) == Some("") {
-			list.pop();
-			true
-		} else {
-			false
-		};
-		modify(&mut list)?;
-		if list.is_empty() {
-			*str = None
-		} else {
-			if had_end_newline {
-				list.push("".to_owned())
-			}
-			*str = Some(list.join("\n"));
-		}
-		Ok(())
-	})
-}
-fn update_section(
-	data: &mut Vec<String>,
-	start: &str,
-	end: &str,
-	modify: impl Fn(&mut Vec<String>) -> Result<()>,
-) -> Result<()> {
-	let first = data
-		.iter()
-		.enumerate()
-		.filter(|(_, v)| *v == start)
-		.at_most_one()
-		.map_err(|_| anyhow::anyhow!("there should be at most one section start"))?
-		.map(|(v, _)| v);
-	let last = data
-		.iter()
-		.enumerate()
-		.filter(|(_, v)| *v == end)
-		.at_most_one()
-		.map_err(|_| anyhow::anyhow!("there should be at most one section end"))?
-		.map(|(v, _)| v);
-
-	match (first, last) {
-		(None, None) => {
-			let mut out = Vec::new();
-			modify(&mut out)?;
-			if out.is_empty() {
-				return Ok(());
-			}
-			data.push(start.to_owned());
-			data.extend(out);
-			data.push(end.to_owned());
-			Ok(())
-		}
-		(None, Some(_)) | (Some(_), None) => {
-			bail!("mismatched section start/end")
-		}
-		(Some(first), Some(last)) => {
-			ensure!(first < last, "section end should come after start");
-			let mut out = data[first + 1..last]
-				.iter()
-				.map(|v| v.to_owned())
-				.collect_vec();
-			modify(&mut out)?;
-			if out.is_empty() {
-				data.drain(first..=last);
-			} else {
-				data.splice(first + 1..last, out);
-			}
-			Ok(())
-		}
-	}
-}
-
-struct Group {
-	path: PathBuf,
-}
-impl Group {
-	fn new(path: PathBuf) -> Self {
-		Self { path }
-	}
-	fn manage(&self, manager: &str) {}
-	fn ensure_managing(&self, manager: &str) {
-		if !self.has_stored() {
-			return;
-		}
-		let managed = match fs::read_to_string(self.path.join(".managed_by")) {
-			Ok(found_manager) => found_manager.lines().any(|line| line == manager),
-			Err(e) if e.kind() == ErrorKind::NotFound => true,
-			Err(e) => panic!("{e}"),
-		};
-		assert!(managed);
-	}
-	fn has_stored(&self) -> bool {
-		match fs::metadata(&self.path) {
-			Ok(d) => d.is_dir(),
-			Err(e) if e.kind() == ErrorKind::NotFound => false,
-			Err(e) => panic!("{e}"),
-		}
-	}
-}
-
-struct Root {
-	path: PathBuf,
-}
-impl Root {
-	fn new(path: PathBuf) -> Self {
-		Self { path }
-	}
-	fn subgroup(&self, name: &str) -> Group {
-		Group::new(self.path.join(name))
-	}
-}
-
-#[test]
-fn test() {
-	let mut data = vec![
-		"a".to_owned(),
-		"b".to_owned(),
-		"start".to_owned(),
-		"c".to_owned(),
-		"d".to_owned(),
-		"end".to_owned(),
-		"e".to_owned(),
-		"f".to_owned(),
-	];
-	update_section(&mut data, "start", "end", |a| {
-		a.push("vv".to_owned());
-		Ok(())
-	})
-	.unwrap();
-	dbg!(&data);
-	// for v in 0..1000 {
-	// 	update_toml(PathBuf::from("./test.toml"), |e| {
-	// 		e.as_table_mut()
-	// 			.insert("hello", Item::Value(Value::Integer(Formatted::new(v))));
-	// 	})
-	// 	.expect("update")
-	// }
-	// v.subgroup(name)
-}
modifiedcrates/nix-eval/Cargo.tomldiffbeforeafterboth
--- a/crates/nix-eval/Cargo.toml
+++ b/crates/nix-eval/Cargo.toml
@@ -10,10 +10,11 @@
 nixlike.workspace = true
 serde = { workspace = true, features = ["derive"] }
 serde_json.workspace = true
-thiserror.workspace = true
 tracing.workspace = true
 
+camino = { workspace = true, features = ["serde1"] }
 cxx.workspace = true
+futures.workspace = true
 itertools.workspace = true
 test-log.workspace = true
 tokio.workspace = true
modifiedcrates/nix-eval/src/drv.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/drv.rs
+++ b/crates/nix-eval/src/drv.rs
@@ -66,7 +66,13 @@
 #[derive(Debug, Deserialize)]
 pub struct DrvParsed {
 	pub inputs: DrvInputs,
-	pub outputs: HashMap<String, serde_json::Value>,
+	pub outputs: HashMap<String, DrvParsedOutput>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct DrvParsedOutput {
+	#[serde(default)]
+	pub path: Option<String>,
 }
 
 #[derive(Debug, Deserialize)]
@@ -82,18 +88,19 @@
 	pub outputs: Vec<String>,
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct DrvGraph {
 	pub root: String,
 	pub nodes: HashMap<String, DrvNode>,
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct DrvNode {
 	pub name: String,
 	pub input_drvs: HashMap<String, Vec<String>>,
 	pub input_srcs: Vec<String>,
-	pub outputs: Vec<String>,
+	// TODO: CA outputs without a known paths are skipped
+	pub outputs: HashMap<String, String>,
 }
 
 impl DrvGraph {
@@ -124,19 +131,59 @@
 				}
 			}
 
+			let outputs: HashMap<String, String> = parsed
+				.outputs
+				.into_iter()
+				.filter_map(|(name, out)| out.path.map(|p| (name, to_absolute_store_path(&sd, &p))))
+				.collect();
+
 			nodes.insert(
 				path.clone(),
 				DrvNode {
 					name: extract_drv_name(&path),
 					input_drvs,
 					input_srcs: parsed.inputs.srcs,
-					outputs: parsed.outputs.into_keys().collect(),
+					outputs,
 				},
 			);
 		}
 
 		Ok(Self { root, nodes })
 	}
+
+	pub fn wanted_outputs(&self, root_outputs: &[String]) -> HashMap<String, Vec<String>> {
+		let mut wanted: HashMap<String, HashSet<String>> = HashMap::new();
+		wanted.insert(self.root.clone(), root_outputs.iter().cloned().collect());
+
+		let mut queue: VecDeque<String> = VecDeque::new();
+		queue.push_back(self.root.clone());
+		while let Some(path) = queue.pop_front() {
+			let Some(node) = self.nodes.get(&path) else {
+				continue;
+			};
+			for (dep_path, dep_outputs) in &node.input_drvs {
+				let entry = wanted.entry(dep_path.clone()).or_default();
+				let mut changed = false;
+				for o in dep_outputs {
+					if entry.insert(o.clone()) {
+						changed = true;
+					}
+				}
+				if changed {
+					queue.push_back(dep_path.clone());
+				}
+			}
+		}
+
+		wanted
+			.into_iter()
+			.map(|(k, v)| {
+				let mut v: Vec<_> = v.into_iter().collect();
+				v.sort();
+				(k, v)
+			})
+			.collect()
+	}
 }
 
 fn extract_drv_name(drv_path: &str) -> String {
modifiedcrates/nix-eval/src/lib.ccdiffbeforeafterboth
--- a/crates/nix-eval/src/lib.cc
+++ b/crates/nix-eval/src/lib.cc
@@ -1,8 +1,25 @@
 #include "nix-eval/src/lib.rs"
 #include "lib.hh"
 #include <nix/fetchers/fetch-settings.hh>
+#include <nix/store/build-result.hh>
+#include <nix/store/content-address.hh>
+#include <nix/store/derived-path.hh>
+#include <nix/store/local-fs-store.hh>
+#include <nix/store/outputs-spec.hh>
+#include <nix/store/path-info.hh>
+#include <nix/store/profiles.hh>
+#include <nix/store/realisation.hh>
+#include <nix/store/store-api.hh>
+#include <nix/util/file-system.hh>
+#include <nix/util/hash.hh>
+#include <nix/util/posix-source-accessor.hh>
 #include <nix/util/ref.hh>
+#include <nix/util/signature/local-keys.hh>
+#include <nix/util/signature/signer.hh>
+#include <nix/util/source-path.hh>
 #include <nix_api_fetchers.h>
+#include <nix_api_store_internal.h>
+#include <sstream>
 
 struct nix_fetchers_settings {
   nix::ref<nix::fetchers::Settings> settings;
@@ -15,3 +32,175 @@
   bool result = settings_ref->set(setting, value);
 }
 }
+
+rust::String switch_profile(Store *store, rust::Str profile,
+                            rust::Str store_path) {
+  try {
+    auto nixStore = store->ptr;
+    auto *lfs = dynamic_cast<nix::LocalFSStore *>(&*nixStore);
+    if (!lfs)
+      return rust::String("destination is not a local-fs store");
+    auto path = nixStore->parseStorePath(std::string(store_path));
+    std::filesystem::path prof = std::string(profile);
+    auto gen = nix::createGeneration(*lfs, prof, path);
+    nix::switchLink(prof, gen);
+    return rust::String();
+  } catch (const std::exception &e) {
+    return rust::String(e.what());
+  }
+}
+
+rust::String sign_closure(Store *store, rust::Str store_path,
+                          rust::Str key_file) {
+  try {
+    auto nixStore = store->ptr;
+    nix::LocalSigner signer(
+        nix::SecretKey::parse(nix::readFile(std::string(key_file))));
+    auto root = nixStore->parseStorePath(std::string(store_path));
+    nix::StorePathSet closure;
+    nixStore->computeFSClosure(root, closure);
+    for (auto &p : closure) {
+      auto info = nixStore->queryPathInfo(p);
+      nix::ValidPathInfo info2(*info);
+      info2.sign(*nixStore, signer);
+      nixStore->addSignatures(p, info2.sigs);
+    }
+    return rust::String();
+  } catch (const std::exception &e) {
+    return rust::String(e.what());
+  }
+}
+
+CxxListGenerationsResult list_generations(rust::Str profile_path) {
+  CxxListGenerationsResult out{rust::String(), {}};
+  try {
+    auto [gens, current] =
+        nix::findGenerations(std::filesystem::path(std::string(profile_path)));
+    for (auto &g : gens) {
+      CxxProfileGeneration cg{};
+      cg.id = g.number;
+      cg.store_path = rust::String(g.path.string());
+      cg.creation_time_unix = static_cast<int64_t>(g.creationTime);
+      cg.current = current.has_value() && *current == g.number;
+      out.generations.push_back(cg);
+    }
+  } catch (const std::exception &e) {
+    out.error = rust::String(e.what());
+  }
+  return out;
+}
+
+static std::vector<std::string> split_lines(rust::Str joined) {
+  std::vector<std::string> out;
+  std::string buf;
+  std::istringstream iss((std::string(joined)));
+  while (std::getline(iss, buf)) {
+    if (!buf.empty())
+      out.push_back(buf);
+  }
+  return out;
+}
+
+CxxBuildResult build_drv_outputs(Store *store, rust::Str drv_path,
+                                 // TODO: Vec<str>
+                                 rust::Str output_names_joined) {
+  CxxBuildResult res{rust::String(), {}};
+  try {
+    auto nixStore = store->ptr;
+    auto sp = nixStore->parseStorePath(std::string(drv_path));
+
+    nix::DerivedPath::Built built{
+        .drvPath = nix::makeConstantStorePathRef(sp),
+        .outputs = nix::OutputsSpec{nix::OutputsSpec::All{}},
+    };
+    auto names = split_lines(output_names_joined);
+    if (!names.empty()) {
+      std::set<nix::OutputName, std::less<>> nameSet;
+      for (auto &n : names)
+        nameSet.insert(n);
+      built.outputs =
+          nix::OutputsSpec{nix::OutputsSpec::Names{std::move(nameSet)}};
+    }
+
+    std::vector<nix::DerivedPath> reqs;
+    reqs.push_back(built);
+
+    auto results = nixStore->buildPathsWithResults(reqs);
+    for (auto &r : results) {
+      if (auto *failure = r.tryGetFailure()) {
+        res.error = rust::String(failure->what());
+        return res;
+      }
+    }
+    for (auto &r : results) {
+      if (auto *success = r.tryGetSuccess()) {
+        for (auto &[name, real] : success->builtOutputs) {
+          res.outputs.push_back(
+              rust::String(nixStore->printStorePath(real.outPath)));
+        }
+      }
+    }
+  } catch (const std::exception &e) {
+    res.error = rust::String(e.what());
+  }
+  return res;
+}
+
+CxxBuildResult substitute_paths(Store *store, rust::Str paths_joined) {
+  CxxBuildResult res{rust::String(), {}};
+  try {
+    auto nixStore = store->ptr;
+    auto paths = split_lines(paths_joined);
+
+    std::vector<nix::DerivedPath> reqs;
+    reqs.reserve(paths.size());
+    for (auto &p : paths) {
+      reqs.push_back(nix::DerivedPath::Opaque{nixStore->parseStorePath(p)});
+    }
+
+    // Substituter miss will cause scheduler to trigger build
+    auto results = nixStore->buildPathsWithResults(reqs);
+    for (auto &r : results) {
+      if (r.tryGetSuccess() == nullptr)
+        continue;
+      if (auto *opaque = std::get_if<nix::DerivedPath::Opaque>(&r.path.raw())) {
+        res.outputs.push_back(
+            rust::String(nixStore->printStorePath(opaque->path)));
+      }
+    }
+  } catch (const std::exception &e) {
+    res.error = rust::String(e.what());
+  }
+  return res;
+}
+
+bool is_valid_path(Store *store, rust::Str path) {
+  try {
+    auto nixStore = store->ptr;
+    auto sp = nixStore->parseStorePath(std::string(path));
+    return nixStore->isValidPath(sp);
+  } catch (const std::exception &) {
+    return false;
+  }
+}
+
+AddFileToStoreResult add_file_to_store(Store *store, rust::Str name,
+                                       rust::Str path) {
+  AddFileToStoreResult out{rust::String(), rust::String(), rust::String()};
+  try {
+    auto nixStore = store->ptr;
+    auto src = nix::PosixSourceAccessor::createAtRoot(
+        std::filesystem::path(std::string(path)));
+    auto info = nixStore->addToStoreSlow(std::string(name), src,
+                                         nix::ContentAddressMethod::Raw::Flat,
+                                         nix::HashAlgorithm::SHA256);
+    out.store_path = rust::String(nixStore->printStorePath(info.path));
+    if (info.ca.has_value()) {
+      out.hash =
+          rust::String(info.ca->hash.to_string(nix::HashFormat::SRI, true));
+    }
+  } catch (const std::exception &e) {
+    out.error = rust::String(e.what());
+  }
+  return out;
+}
modifiedcrates/nix-eval/src/lib.hhdiffbeforeafterboth
--- a/crates/nix-eval/src/lib.hh
+++ b/crates/nix-eval/src/lib.hh
@@ -1,7 +1,32 @@
 #pragma once
+#include "rust/cxx.h"
 #include <nix_api_fetchers.h>
 
+struct Store;
+
+struct AddFileToStoreResult;
+struct CxxBuildResult;
+
 extern "C" {
 void set_fetcher_setting(nix_fetchers_settings *settings, const char *setting,
                          const char *value);
 }
+
+rust::String switch_profile(Store *store, rust::Str profile,
+                            rust::Str store_path);
+
+rust::String sign_closure(Store *store, rust::Str store_path,
+                          rust::Str key_file);
+
+struct CxxListGenerationsResult;
+CxxListGenerationsResult list_generations(rust::Str profile_path);
+
+AddFileToStoreResult add_file_to_store(Store *store, rust::Str name,
+                                       rust::Str path);
+
+CxxBuildResult build_drv_outputs(Store *store, rust::Str drv_path,
+                                 rust::Str output_names_joined);
+
+CxxBuildResult substitute_paths(Store *store, rust::Str paths_joined);
+
+bool is_valid_path(Store *store, rust::Str path);
modifiedcrates/nix-eval/src/lib.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/lib.rs
+++ b/crates/nix-eval/src/lib.rs
@@ -1,12 +1,13 @@
 use std::borrow::Cow;
 use std::cell::RefCell;
+use std::collections::HashMap;
 use std::ffi::{CStr, CString, c_char, c_int, c_uint, c_void};
 use std::ptr::{null, null_mut};
 use std::sync::{Arc, LazyLock, OnceLock};
 use std::{array, fmt, slice};
-use std::{collections::HashMap, path::PathBuf};
 
 use anyhow::{Context, anyhow, bail};
+use camino::{Utf8Path, Utf8PathBuf};
 use itertools::Itertools;
 use serde::Serialize;
 use serde::de::DeserializeOwned;
@@ -38,8 +39,9 @@
 	make_attrs, make_bindings_builder, make_list, make_list_builder, realised_string,
 	realised_string_free, realised_string_get_buffer_size, realised_string_get_buffer_start,
 	realised_string_get_store_path, realised_string_get_store_path_count, register_primop,
-	set_err_msg, setting_set, state_free, store_open, store_parse_path, store_path_free,
-	store_path_name, string_realise, value, value_call, value_decref, value_force, value_incref,
+	set_err_msg, setting_set, state_free, store_copy_closure, store_free, store_open,
+	store_parse_path, store_path_free, store_path_name, string_realise, value, value_call,
+	value_decref, value_force, value_incref,
 };
 
 // Contains macros helpers
@@ -47,6 +49,7 @@
 pub mod logging;
 #[doc(hidden)]
 pub mod macros;
+pub mod scheduler;
 
 #[doc(hidden)]
 pub mod __macro_support {
@@ -68,8 +71,28 @@
 }
 #[cxx::bridge]
 pub mod nix_cxx {
+	struct AddFileToStoreResult {
+		error: String,
+		store_path: String,
+		hash: String,
+	}
+	struct CxxProfileGeneration {
+		id: u64,
+		store_path: String,
+		creation_time_unix: i64,
+		current: bool,
+	}
+	struct CxxListGenerationsResult {
+		error: String,
+		generations: Vec<CxxProfileGeneration>,
+	}
+	struct CxxBuildResult {
+		error: String,
+		outputs: Vec<String>,
+	}
 	unsafe extern "C++" {
 		type nix_fetchers_settings;
+		type Store;
 		include!("nix-eval/src/lib.hh");
 
 		#[allow(clippy::missing_safety_doc)]
@@ -78,6 +101,34 @@
 			setting: *const c_char,
 			value: *const c_char,
 		);
+
+		#[allow(clippy::missing_safety_doc)]
+		unsafe fn switch_profile(store: *mut Store, profile: &str, store_path: &str) -> String;
+
+		#[allow(clippy::missing_safety_doc)]
+		unsafe fn sign_closure(store: *mut Store, store_path: &str, key_file: &str) -> String;
+
+		#[allow(clippy::missing_safety_doc)]
+		unsafe fn add_file_to_store(
+			store: *mut Store,
+			name: &str,
+			path: &str,
+		) -> AddFileToStoreResult;
+
+		fn list_generations(profile_path: &str) -> CxxListGenerationsResult;
+
+		#[allow(clippy::missing_safety_doc)]
+		unsafe fn build_drv_outputs(
+			store: *mut Store,
+			drv_path: &str,
+			output_names_joined: &str,
+		) -> CxxBuildResult;
+
+		#[allow(clippy::missing_safety_doc)]
+		unsafe fn substitute_paths(store: *mut Store, paths_joined: &str) -> CxxBuildResult;
+
+		#[allow(clippy::missing_safety_doc)]
+		unsafe fn is_valid_path(store: *mut Store, path: &str) -> bool;
 	}
 }
 
@@ -186,7 +237,7 @@
 		unsafe { set_err_msg(self.0, err as c_int, msg.as_ptr()) };
 	}
 	pub fn set_err(&mut self, err: anyhow::Error) {
-		let mut fmt = format!("{err:?}").replace("\0", "\\0");
+		let fmt = format!("{err:?}").replace("\0", "\\0");
 		self.set_err_raw(
 			NixErrorKind::Generic,
 			&CString::new(fmt).expect("NUL bytes were just replaced"),
@@ -352,6 +403,125 @@
 	with_default_context(|c, _| unsafe { setting_set(c, s.as_ptr(), v.as_ptr()) }).map(|_| ())
 }
 
+#[instrument(skip(dst))]
+pub fn copy_closure_to(dst: &Store, path: &Utf8Path) -> Result<()> {
+	let path_c = CString::new(path.as_str())?;
+	with_store_context(|c, src_store, _state| -> Result<()> {
+		let sp = unsafe { store_parse_path(c, src_store, path_c.as_ptr()) };
+		if sp.is_null() {
+			bail!("failed to parse store path {path}");
+		}
+		let rc = unsafe { store_copy_closure(c, src_store, dst.0, sp) };
+		unsafe { store_path_free(sp) };
+		if rc != nix_raw::err_NIX_OK {
+			bail!("store_copy_closure failed (code {rc})");
+		}
+		Ok(())
+	})?
+}
+
+#[instrument]
+pub fn switch_profile(profile: &str, store_path: &Utf8Path) -> Result<()> {
+	let msg = with_store_context(|_c, store, _state| unsafe {
+		nix_cxx::switch_profile(store.cast(), profile, store_path.as_str())
+	})?
+	.to_string();
+	if msg.is_empty() {
+		Ok(())
+	} else {
+		bail!("failed to switch profile {profile}: {msg}");
+	}
+}
+
+// TODO: fleet operator-managed key file
+#[instrument]
+pub fn sign_closure(store_path: &str, key_file: &str) -> Result<()> {
+	let msg = with_store_context(|_c, store, _state| unsafe {
+		nix_cxx::sign_closure(store.cast(), store_path, key_file)
+	})?
+	.to_string();
+	if msg.is_empty() {
+		Ok(())
+	} else {
+		bail!("failed to sign {store_path}: {msg}");
+	}
+}
+
+#[derive(Debug)]
+pub struct AddedFile {
+	pub store_path: Utf8PathBuf,
+	pub hash: String,
+}
+
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+pub struct ProfileGeneration {
+	pub id: u64,
+	pub store_path: Utf8PathBuf,
+	pub creation_time_unix: i64,
+	pub current: bool,
+}
+
+#[instrument]
+pub fn list_generations(profile_path: &str) -> Result<Vec<ProfileGeneration>> {
+	let res = nix_cxx::list_generations(profile_path);
+	if !res.error.is_empty() {
+		bail!(
+			"failed to list generations at {profile_path}: {}",
+			res.error
+		);
+	}
+	Ok(res
+		.generations
+		.into_iter()
+		.map(|g| ProfileGeneration {
+			id: g.id,
+			store_path: Utf8PathBuf::from(g.store_path),
+			creation_time_unix: g.creation_time_unix,
+			current: g.current,
+		})
+		.collect())
+}
+
+#[instrument]
+pub fn add_file_to_store(name: &str, path: &Utf8Path) -> Result<AddedFile> {
+	let res = with_store_context(|_c, store, _state| unsafe {
+		nix_cxx::add_file_to_store(store.cast(), name, path.as_str())
+	})?;
+	if !res.error.is_empty() {
+		bail!("failed to add {path} to store: {}", res.error);
+	}
+	Ok(AddedFile {
+		store_path: Utf8PathBuf::from(res.store_path),
+		hash: res.hash,
+	})
+}
+
+pub fn build_drv_outputs(drv_path: &str, output_names: &[String]) -> Result<Vec<String>> {
+	let joined = output_names.join("\n");
+	let res = with_store_context(|_c, store, _state| unsafe {
+		nix_cxx::build_drv_outputs(store.cast(), drv_path, &joined)
+	})?;
+	if !res.error.is_empty() {
+		bail!("build of {drv_path} failed: {}", res.error);
+	}
+	Ok(res.outputs)
+}
+
+pub fn substitute_paths(paths: &[String]) -> Result<Vec<String>> {
+	let joined = paths.join("\n");
+	let res = with_store_context(|_c, store, _state| unsafe {
+		nix_cxx::substitute_paths(store.cast(), &joined)
+	})?;
+	if !res.error.is_empty() {
+		warn!("substitute_paths reported: {}", res.error);
+	}
+	Ok(res.outputs)
+}
+
+pub fn is_valid_path(path: &str) -> Result<bool> {
+	with_store_context(|_c, store, _state| unsafe { nix_cxx::is_valid_path(store.cast(), path) })
+}
+
 pub struct FetchSettings(*mut fetchers_settings);
 impl FetchSettings {
 	pub fn new() -> Self {
@@ -450,17 +620,31 @@
 	unsafe { *user_data.cast::<String>() = s.to_owned() };
 }
 
-struct Store(*mut c_store);
+pub struct Store(*mut c_store);
 unsafe impl Send for Store {}
 unsafe impl Sync for Store {}
 
 impl Store {
+	pub fn open(uri: &str) -> Result<Self> {
+		let uri = CString::new(uri)?;
+		let ptr = with_default_context(|c, _| unsafe { store_open(c, uri.as_ptr(), null_mut()) })?;
+		if ptr.is_null() {
+			bail!("failed to open store");
+		}
+		Ok(Store(ptr))
+	}
+
 	fn parse_path(&self, path: &CStr) -> Result<StorePath> {
 		with_default_context(|c, _| {
 			StorePath(unsafe { store_parse_path(c, self.0, path.as_ptr()) })
 		})
 	}
 }
+impl Drop for Store {
+	fn drop(&mut self) {
+		unsafe { store_free(self.0) }
+	}
+}
 
 #[repr(transparent)]
 pub struct EvalState(*mut c_eval_state);
@@ -858,7 +1042,7 @@
 		Ok(out)
 	}
 	#[instrument(name = "build", skip(self), fields(output))]
-	pub fn build(&self, output: &str) -> Result<PathBuf> {
+	pub fn build(&self, output: &str) -> Result<Utf8PathBuf> {
 		if !self.is_derivation() {
 			bail!("expected derivation to build")
 		}
@@ -880,14 +1064,15 @@
 			.get_field("drvPath")
 			.context("getting drvPath")?
 			.to_string()?;
-		let graph = drv::DrvGraph::resolve(&drv_path)?;
+		let graph = Arc::new(drv::DrvGraph::resolve(&drv_path)?);
 		let _guard = logging::register_build_graph(&Span::current(), &graph);
 
-		// to_string here blocks until the path is built
+		scheduler::build_graph_sync(graph.clone(), vec![output.to_owned()])?;
+
 		let s = v.builtin_to_string()?;
 		let rs = s.to_realised_string()?;
 		let out_path = rs.as_str().to_owned();
-		Ok(PathBuf::from(out_path))
+		Ok(Utf8PathBuf::from(out_path))
 	}
 	pub fn as_json<T: DeserializeOwned>(&self) -> Result<T> {
 		let to_json = Self::eval("builtins.toJSON")?;
modifiedcrates/nix-eval/src/logging.ccdiffbeforeafterboth
--- a/crates/nix-eval/src/logging.cc
+++ b/crates/nix-eval/src/logging.cc
@@ -96,7 +96,9 @@
 
 extern "C" {
 void apply_tracing_logger() {
-  logger = std::make_unique<TracingLogger>();
+  auto old = logger;
+  logger = new TracingLogger();
+  delete old;
   // verbosity = lvlVomit;
 }
 rust::Box<ErrorInfoBuilder>
modifiedcrates/nix-eval/src/logging.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/logging.rs
+++ b/crates/nix-eval/src/logging.rs
@@ -499,7 +499,9 @@
 			} else if level == Level::INFO {
 				info!(target: "nix", "{}", s)
 			} else if level == Level::DEBUG {
-				debug!(target: "nix", "{}", s)
+				if s != "querying info about missing paths" {
+					debug!(target: "nix", "{}", s)
+				}
 			} else {
 				trace!(target: "nix", "{}", s)
 			}
@@ -583,7 +585,9 @@
 	} else if level == Level::INFO {
 		info!(target: "nix", "{v}")
 	} else if level == Level::DEBUG {
-		debug!(target: "nix", "{v}")
+		if v != "querying info about missing paths" {
+			debug!(target: "nix", "{v}")
+		}
 	} else {
 		trace!(target: "nix", "{v}")
 	}
addedcrates/nix-eval/src/scheduler.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/nix-eval/src/scheduler.rs
@@ -0,0 +1,372 @@
+use std::collections::{HashMap, HashSet};
+use std::sync::Arc;
+
+use anyhow::{Context, Result, bail};
+use futures::stream::{FuturesUnordered, StreamExt};
+use tokio::sync::{Semaphore, broadcast};
+use tracing::{debug, info, instrument, warn};
+
+use crate::drv::DrvGraph;
+
+#[derive(Clone, Debug)]
+pub enum BuildEvent {
+	SubstitutePrepassStarted {
+		paths: usize,
+	},
+	SubstitutePrepassFinished {
+		satisfied: usize,
+	},
+	DrvStarted {
+		drv_path: String,
+		name: String,
+		wanted: Vec<String>,
+	},
+	DrvSkipped {
+		drv_path: String,
+		name: String,
+	},
+	DrvFinished {
+		drv_path: String,
+		name: String,
+	},
+	DrvFailed {
+		drv_path: String,
+		name: String,
+		error: String,
+	},
+	DrvCancelled {
+		drv_path: String,
+		name: String,
+		failed_dep: String,
+	},
+}
+
+pub struct Scheduler {
+	parallelism: usize,
+	events: broadcast::Sender<BuildEvent>,
+}
+
+impl Scheduler {
+	pub fn new(parallelism: usize) -> Self {
+		let parallelism = parallelism.max(1);
+		let (events, _) = broadcast::channel(1024);
+		Self {
+			parallelism,
+			events,
+		}
+	}
+
+	pub fn subscribe(&self) -> broadcast::Receiver<BuildEvent> {
+		self.events.subscribe()
+	}
+
+	#[instrument(name = "scheduler", skip(self, graph), fields(root = %graph.root, nodes = graph.nodes.len()))]
+	pub async fn run(&self, graph: Arc<DrvGraph>, root_outputs: Vec<String>) -> Result<()> {
+		let wanted = graph.wanted_outputs(&root_outputs);
+
+		self.substitute_prepass(&graph, &wanted).await?;
+		self.build_topo(&graph, wanted).await
+	}
+
+	async fn substitute_prepass(
+		&self,
+		graph: &DrvGraph,
+		wanted: &HashMap<String, Vec<String>>,
+	) -> Result<()> {
+		let paths = collect_substitute_paths(graph, wanted);
+		if paths.is_empty() {
+			return Ok(());
+		}
+		let _ = self
+			.events
+			.send(BuildEvent::SubstitutePrepassStarted { paths: paths.len() });
+		debug!("substitute pre-pass: {} paths", paths.len());
+
+		let satisfied = tokio::task::spawn_blocking(move || crate::substitute_paths(&paths))
+			.await
+			.expect("substitute pre-pass task should not panic")?;
+
+		let _ = self.events.send(BuildEvent::SubstitutePrepassFinished {
+			satisfied: satisfied.len(),
+		});
+		Ok(())
+	}
+
+	async fn build_topo(
+		&self,
+		graph: &Arc<DrvGraph>,
+		wanted: HashMap<String, Vec<String>>,
+	) -> Result<()> {
+		let mut indeg: HashMap<String, usize> = graph
+			.nodes
+			.iter()
+			.map(|(k, n)| (k.clone(), n.input_drvs.len()))
+			.collect();
+		let mut dependents: HashMap<String, Vec<String>> = HashMap::new();
+		for (path, node) in &graph.nodes {
+			for dep in node.input_drvs.keys() {
+				dependents
+					.entry(dep.clone())
+					.or_default()
+					.push(path.clone());
+			}
+		}
+
+		let sem = Arc::new(Semaphore::new(self.parallelism));
+		let mut ready: Vec<String> = indeg
+			.iter()
+			.filter(|(_, d)| **d == 0)
+			.map(|(k, _)| k.clone())
+			.collect();
+		let mut in_flight = FuturesUnordered::new();
+		let mut failed: HashMap<String, String> = HashMap::new();
+		// Tainted = transitively depends on a failed drv
+		let mut tainted: HashMap<String, String> = HashMap::new();
+
+		loop {
+			let batch: Vec<String> = std::mem::take(&mut ready);
+			for path in batch {
+				if let Some(failed_dep) = tainted.get(&path) {
+					let name = graph
+						.nodes
+						.get(&path)
+						.map(|n| n.name.clone())
+						.unwrap_or_default();
+					let _ = self.events.send(BuildEvent::DrvCancelled {
+						drv_path: path.clone(),
+						name,
+						failed_dep: failed_dep.clone(),
+					});
+					propagate_done(&dependents, &mut indeg, &mut ready, &path);
+					continue;
+				}
+
+				let sem = sem.clone();
+				let events = self.events.clone();
+				let graph = graph.clone();
+				let wanted_here = wanted.get(&path).cloned().unwrap_or_default();
+				in_flight.push(tokio::spawn(async move {
+					let _permit = sem.acquire_owned().await.expect("semaphore not closed");
+					let node = graph
+						.nodes
+						.get(&path)
+						.expect("ready node must be in graph")
+						.clone();
+					let name = node.name.clone();
+
+					let all_valid = !wanted_here.is_empty()
+						&& wanted_here.iter().all(|o| {
+							node.outputs
+								.get(o)
+								.map(|p| crate::is_valid_path(p).unwrap_or(false))
+								.unwrap_or(false)
+						});
+					if all_valid {
+						let _ = events.send(BuildEvent::DrvSkipped {
+							drv_path: path.clone(),
+							name: name.clone(),
+						});
+						return (path, name, Ok::<(), anyhow::Error>(()));
+					}
+
+					let _ = events.send(BuildEvent::DrvStarted {
+						drv_path: path.clone(),
+						name: name.clone(),
+						wanted: wanted_here.clone(),
+					});
+
+					let path_for_build = path.clone();
+					let res = tokio::task::spawn_blocking(move || {
+						crate::build_drv_outputs(&path_for_build, &wanted_here)
+					})
+					.await
+					.expect("build task should not panic");
+
+					match res {
+						Ok(_) => {
+							let _ = events.send(BuildEvent::DrvFinished {
+								drv_path: path.clone(),
+								name: name.clone(),
+							});
+							(path, name, Ok(()))
+						}
+						Err(e) => {
+							let msg = format!("{e:#}");
+							let _ = events.send(BuildEvent::DrvFailed {
+								drv_path: path.clone(),
+								name: name.clone(),
+								error: msg,
+							});
+							(path, name, Err(e))
+						}
+					}
+				}));
+			}
+
+			let Some(joined) = in_flight.next().await else {
+				break;
+			};
+			let (finished, _name, res) = match joined {
+				Ok(t) => t,
+				Err(e) => bail!("scheduler task panicked: {e}"),
+			};
+			match res {
+				Ok(()) => {
+					propagate_done(&dependents, &mut indeg, &mut ready, &finished);
+				}
+				Err(e) => {
+					failed.insert(finished.clone(), format!("{e:#}"));
+					mark_tainted(&dependents, &finished, &mut tainted);
+					propagate_done(&dependents, &mut indeg, &mut ready, &finished);
+				}
+			}
+		}
+
+		let stuck: Vec<_> = indeg
+			.iter()
+			.filter(|(_, d)| **d != 0)
+			.map(|(k, _)| k.as_str())
+			.collect();
+		if !stuck.is_empty() {
+			warn!(
+				"scheduler finished with {} nodes still pending (loop?)",
+				stuck.len()
+			);
+		}
+
+		if failed.is_empty() {
+			info!("scheduler completed");
+			Ok(())
+		} else {
+			let mut report = format!("{} drv(s) failed to build:", failed.len());
+			let mut sorted: Vec<_> = failed.iter().collect();
+			sorted.sort_by(|a, b| a.0.cmp(b.0));
+			for (path, err) in sorted {
+				let name = graph
+					.nodes
+					.get(path)
+					.map(|n| n.name.as_str())
+					.unwrap_or("?");
+				let chain = path_to_root(graph, path);
+				report.push_str(&format!(
+					"\n\n  {name} ({path}):\n    {err}\n    needed by: {}",
+					chain.join(" => "),
+				));
+			}
+			Err(anyhow::anyhow!(report))
+		}
+	}
+}
+
+fn propagate_done(
+	dependents: &HashMap<String, Vec<String>>,
+	indeg: &mut HashMap<String, usize>,
+	ready: &mut Vec<String>,
+	finished: &str,
+) {
+	if let Some(deps) = dependents.get(finished) {
+		for d in deps {
+			let entry = indeg.get_mut(d).expect("dependent must have indeg");
+			*entry = entry.saturating_sub(1);
+			if *entry == 0 {
+				ready.push(d.clone());
+			}
+		}
+	}
+}
+
+fn mark_tainted(
+	dependents: &HashMap<String, Vec<String>>,
+	failed: &str,
+	tainted: &mut HashMap<String, String>,
+) {
+	let mut queue: Vec<String> = dependents.get(failed).cloned().unwrap_or_default();
+	while let Some(node) = queue.pop() {
+		if tainted
+			.entry(node.clone())
+			.or_insert_with(|| failed.to_owned())
+			== failed
+		{
+			if let Some(deps) = dependents.get(&node) {
+				for d in deps {
+					if !tainted.contains_key(d) {
+						queue.push(d.clone());
+					}
+				}
+			}
+		}
+	}
+}
+
+fn path_to_root(graph: &DrvGraph, from: &str) -> Vec<String> {
+	let mut dependents: HashMap<&str, Vec<&str>> = HashMap::new();
+	for (path, node) in &graph.nodes {
+		for dep in node.input_drvs.keys() {
+			dependents
+				.entry(dep.as_str())
+				.or_default()
+				.push(path.as_str());
+		}
+	}
+
+	let mut chain: Vec<String> = vec![node_name(graph, from)];
+	let mut cur = from;
+	let mut seen: HashSet<&str> = HashSet::new();
+	seen.insert(cur);
+	while cur != graph.root.as_str() {
+		let Some(next) = dependents.get(cur).and_then(|v| v.first().copied()) else {
+			break;
+		};
+		if !seen.insert(next) {
+			break;
+		}
+		chain.push(node_name(graph, next));
+		cur = next;
+	}
+	chain
+}
+
+fn node_name(graph: &DrvGraph, path: &str) -> String {
+	graph
+		.nodes
+		.get(path)
+		.map(|n| n.name.clone())
+		.unwrap_or_else(|| path.to_owned())
+}
+
+fn collect_substitute_paths(
+	graph: &DrvGraph,
+	wanted: &HashMap<String, Vec<String>>,
+) -> Vec<String> {
+	let mut paths: HashSet<String> = HashSet::new();
+	for node in graph.nodes.values() {
+		for src in &node.input_srcs {
+			paths.insert(src.clone());
+		}
+	}
+	for (path, outs) in wanted {
+		let Some(node) = graph.nodes.get(path) else {
+			continue;
+		};
+		for o in outs {
+			if let Some(p) = node.outputs.get(o) {
+				paths.insert(p.clone());
+			}
+		}
+	}
+	let mut v: Vec<_> = paths.into_iter().collect();
+	v.sort();
+	v
+}
+
+// TODO: Parallelism as a metric works poorly with multiple machines, but I haven't thought about bringing
+// hercy here yet. In case of remote machines - they will handle parallelism on their own, and this one
+// will work as a hard cap.
+pub fn build_graph_sync(graph: Arc<DrvGraph>, root_outputs: Vec<String>) -> Result<()> {
+	let parallelism = std::thread::available_parallelism()
+		.map(|p| p.get())
+		.unwrap_or(4);
+	let scheduler = Scheduler::new(parallelism);
+	crate::await_in_nix(async move { scheduler.run(graph, root_outputs).await })
+		.context("scheduler run")
+}
modifiedcrates/nixlike/Cargo.tomldiffbeforeafterboth
--- a/crates/nixlike/Cargo.toml
+++ b/crates/nixlike/Cargo.toml
@@ -11,5 +11,3 @@
 linked-hash-map.workspace = true
 peg.workspace = true
 serde.workspace = true
-serde-transcode.workspace = true
-serde_json.workspace = true
addedcrates/remowt-fleet/Cargo.tomldiffbeforeafterboth
--- /dev/null
+++ b/crates/remowt-fleet/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "remowt-fleet"
+description = "Remowt endpoints for fleet"
+version.workspace = true
+edition = "2021"
+
+[dependencies]
+bifrostlink.workspace = true
+
+remowt-client.workspace = true
+remowt-endpoints.workspace = true
+
+anyhow.workspace = true
+camino.workspace = true
+nix-eval.workspace = true
+serde = { workspace = true }
+thiserror.workspace = true
+tokio = { workspace = true, features = ["io-util", "net", "process", "rt"] }
+tracing.workspace = true
+uuid.workspace = true
addedcrates/remowt-fleet/src/lib.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/remowt-fleet/src/lib.rs
@@ -0,0 +1,120 @@
+use std::path::PathBuf;
+
+use anyhow::{Context as _, Result};
+use bifrostlink::Config;
+use bifrostlink::declarative::endpoints;
+use camino::Utf8PathBuf;
+use remowt_client::Remowt;
+use remowt_endpoints::nix_daemon::NixDaemonClient;
+use serde::{Deserialize, Serialize};
+use tokio::net::UnixListener;
+use tracing::error;
+
+pub struct Nix;
+pub use nix_eval::{init_libraries, init_tokio_for_nix};
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum NixError {
+	#[error("nix daemon unavailable: {0}")]
+	DaemonUnavailable(String),
+	#[error("tunnel socket unavailable: {0}")]
+	Tunnel(String),
+	#[error("profile switch failed: {0}")]
+	Profile(String),
+	#[error("signing failed: {0}")]
+	Sign(String),
+	#[error("listing generations failed: {0}")]
+	ListGenerations(String),
+}
+
+#[endpoints(ns = 91)]
+impl Nix {
+	#[endpoints(id = 3)]
+	async fn switch_profile(
+		&self,
+		profile: String,
+		store_path: Utf8PathBuf,
+	) -> Result<(), NixError> {
+		tokio::task::spawn_blocking(move || nix_eval::switch_profile(&profile, &store_path))
+			.await
+			.map_err(|e| NixError::Profile(e.to_string()))?
+			.map_err(|e| NixError::Profile(e.to_string()))
+	}
+
+	#[endpoints(id = 4)]
+	async fn sign_closure(
+		&self,
+		store_path: Utf8PathBuf,
+		key_file: Utf8PathBuf,
+	) -> Result<(), NixError> {
+		tokio::task::spawn_blocking(move || {
+			nix_eval::sign_closure(store_path.as_str(), key_file.as_str())
+		})
+		.await
+		.map_err(|e| NixError::Sign(e.to_string()))?
+		.map_err(|e| NixError::Sign(e.to_string()))
+	}
+
+	#[endpoints(id = 5)]
+	async fn list_generations(
+		&self,
+		profile: String,
+	) -> Result<Vec<nix_eval::ProfileGeneration>, NixError> {
+		tokio::task::spawn_blocking(move || {
+			nix_eval::list_generations(&format!("/nix/var/nix/profiles/{profile}"))
+		})
+		.await
+		.map_err(|e| NixError::ListGenerations(e.to_string()))?
+		.map_err(|e| NixError::ListGenerations(e.to_string()))
+	}
+}
+
+pub async fn nix_store_socket(conn: Remowt, store: &str) -> Result<PathBuf> {
+	let store = store.to_owned();
+	let path = std::env::temp_dir().join(format!("fleet-nix-{}.sock", uuid::Uuid::new_v4()));
+	let _ = std::fs::remove_file(&path);
+	let listener = UnixListener::bind(&path)?;
+	tokio::spawn(async move {
+		if let Err(e) = serve(conn, listener, store).await {
+			error!("nix daemon proxy failed: {e}");
+		}
+	});
+	Ok(path)
+}
+
+async fn serve(conn: Remowt, listener: UnixListener, store: String) -> Result<()> {
+	let nix = conn.endpoints::<NixDaemonClient<_>>();
+	loop {
+		let (mut local, _) = listener.accept().await?;
+
+		let (rx, remote_sock) = match conn.bind_runtime_unix("nix-daemon").await {
+			Ok(rx) => rx,
+			Err(e) => {
+				error!("streamlocal_forward failed: {e}");
+				continue;
+			}
+		};
+		let sock_str = remote_sock.as_str().to_owned();
+		match nix.serve_store(store.clone(), sock_str).await {
+			Ok(Ok(())) => {}
+			Ok(Err(e)) => {
+				error!("nix bridge: {e}");
+				continue;
+			}
+			Err(e) => {
+				error!("nix bridge rpc failed: {e}");
+				continue;
+			}
+		}
+
+		let mut channel = rx
+			.accept()
+			.await
+			.context("failed to accept remote nix connection")?;
+		tokio::spawn(async move {
+			if let Err(e) = tokio::io::copy_bidirectional(&mut local, &mut channel).await {
+				tracing::debug!("nix tunnel ended: {e}");
+			}
+		});
+	}
+}
modifiedflake.lockdiffbeforeafterboth
--- a/flake.lock
+++ b/flake.lock
@@ -2,10 +2,10 @@
   "nodes": {
     "crane": {
       "locked": {
-        "lastModified": 1773189535,
+        "lastModified": 1780532242,
         "owner": "ipetkov",
         "repo": "crane",
-        "rev": "6fa2fb4cf4a89ba49fc9dd5a3eb6cde99d388269",
+        "rev": "59a82a1222dd3b2080b5cc52a1a2e8d5f1b77f37",
         "type": "github"
       },
       "original": {
@@ -37,10 +37,10 @@
         ]
       },
       "locked": {
-        "lastModified": 1772408722,
+        "lastModified": 1778716662,
         "owner": "hercules-ci",
         "repo": "flake-parts",
-        "rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3",
+        "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
         "type": "github"
       },
       "original": {
@@ -62,10 +62,10 @@
         ]
       },
       "locked": {
-        "lastModified": 1759080490,
+        "lastModified": 1780160585,
         "owner": "CertainLach",
         "repo": "fleet-tf",
-        "rev": "878bd8c23933d628bf750378bbe527b841901c3d",
+        "rev": "aa2200a3f8f93a8c0d3b87bb29f11d9d9b0a23e6",
         "type": "github"
       },
       "original": {
@@ -111,10 +111,10 @@
         "nixpkgs-regression": "nixpkgs-regression"
       },
       "locked": {
-        "lastModified": 1768702010,
+        "lastModified": 1781448983,
         "owner": "deltarocks",
         "repo": "nix",
-        "rev": "b05b52670b9c7affff5b9be3edb539a1603c39e6",
+        "rev": "bb8784f285658e77642437409824619f5c34a854",
         "type": "github"
       },
       "original": {
@@ -126,10 +126,10 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1773268153,
+        "lastModified": 1781402740,
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "63ca4f465b79edbaa9ff113ea7ff48fb49ea0db0",
+        "rev": "49145c5aba740948d6da1265f584826bd27619ad",
         "type": "github"
       },
       "original": {
@@ -171,6 +171,34 @@
         "type": "github"
       }
     },
+    "remowt-agents": {
+      "inputs": {
+        "crane": [
+          "crane"
+        ],
+        "flake-parts": [
+          "flake-parts"
+        ],
+        "nixpkgs": [
+          "nixpkgs"
+        ],
+        "rust-overlay": "rust-overlay",
+        "shelly": [
+          "shelly"
+        ]
+      },
+      "locked": {
+        "lastModified": 1781471358,
+        "rev": "c69dddc33f8e2081ef685ae20a7278f92cfcd32e",
+        "revCount": 37,
+        "type": "git",
+        "url": "file:/home/lach/build/remowt-agents"
+      },
+      "original": {
+        "type": "git",
+        "url": "file:/home/lach/build/remowt-agents"
+      }
+    },
     "root": {
       "inputs": {
         "crane": "crane",
@@ -178,7 +206,8 @@
         "fleet-tf": "fleet-tf",
         "nix": "nix",
         "nixpkgs": "nixpkgs",
-        "rust-overlay": "rust-overlay",
+        "remowt-agents": "remowt-agents",
+        "rust-overlay": "rust-overlay_2",
         "shelly": "shelly",
         "treefmt-nix": "treefmt-nix"
       }
@@ -186,14 +215,15 @@
     "rust-overlay": {
       "inputs": {
         "nixpkgs": [
+          "remowt-agents",
           "nixpkgs"
         ]
       },
       "locked": {
-        "lastModified": 1773216618,
+        "lastModified": 1769309768,
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "07d7dc6fcc5eae76b4fb0e19d4afd939437bec97",
+        "rev": "140c9dc582cb73ada2d63a2180524fcaa744fad5",
         "type": "github"
       },
       "original": {
@@ -202,6 +232,25 @@
         "type": "github"
       }
     },
+    "rust-overlay_2": {
+      "inputs": {
+        "nixpkgs": [
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1781320681,
+        "owner": "oxalica",
+        "repo": "rust-overlay",
+        "rev": "5b929d8c854149d926d05ea0cd6469bf4e54db27",
+        "type": "github"
+      },
+      "original": {
+        "owner": "oxalica",
+        "repo": "rust-overlay",
+        "type": "github"
+      }
+    },
     "shelly": {
       "locked": {
         "lastModified": 1756323923,
@@ -223,10 +272,10 @@
         ]
       },
       "locked": {
-        "lastModified": 1772660329,
+        "lastModified": 1780220602,
         "owner": "numtide",
         "repo": "treefmt-nix",
-        "rev": "3710e0e1218041bbad640352a0440114b1e10428",
+        "rev": "db947814a175b7ca6ded66e21383d938df01c227",
         "type": "github"
       },
       "original": {
modifiedflake.nixdiffbeforeafterboth
--- a/flake.nix
+++ b/flake.nix
@@ -23,6 +23,13 @@
       url = "github:numtide/treefmt-nix";
       inputs.nixpkgs.follows = "nixpkgs";
     };
+    remowt-agents = {
+      url = "git+file:/home/lach/build/remowt-agents";
+      inputs.nixpkgs.follows = "nixpkgs";
+      inputs.flake-parts.follows = "flake-parts";
+      inputs.crane.follows = "crane";
+      inputs.shelly.follows = "shelly";
+    };
     # DeterminateSystem's nix fork is controversial, but I don't mind it,
     # and it has lazy-trees support which is useful for fleet.
     nix = {
@@ -147,7 +154,7 @@
             packages = lib.mkIf deployerSystem (
               let
                 packages = pkgs.callPackages ./pkgs {
-                  inherit craneLib inputs';
+                  inherit craneLib inputs;
                 };
               in
               packages // { default = packages.fleet; }
modifiedlib/flakePart.nixdiffbeforeafterboth
--- a/lib/flakePart.nix
+++ b/lib/flakePart.nix
@@ -57,7 +57,7 @@
             modules = (import ../modules/module-list.nix) ++ [
               module
               (
-                { inputs', ... }:
+                { inputs, ... }:
                 {
                   config = {
                     data = if isPath data then import data else data;
@@ -66,12 +66,13 @@
                       (final: prev: {
                         inherit
                           (import ../pkgs {
-                            inherit (prev) callPackage;
-                            inherit inputs';
+                            inherit (final) callPackage;
+                            inherit inputs;
                             craneLib = crane.mkLib prev;
                           })
                           fleet-install-secrets
                           fleet-generator-helper
+                          remowt-plugin-fleet
                           ;
                       })
                     ];
modifiedmodules/nixpkgs.nixdiffbeforeafterboth
--- a/modules/nixpkgs.nix
+++ b/modules/nixpkgs.nix
@@ -42,10 +42,12 @@
         default = config.nixpkgs.buildUsing;
         defaultText = literalExpression "config.nixpkgs.buildUsing";
       };
-      config.nixos = {
-        inherit _file;
-        nixpkgs.overlays = config.nixpkgs.overlays;
-      };
+      config.nixos =
+        { inputs', ... }:
+        {
+          inherit _file;
+          nixpkgs.overlays = config.nixpkgs.overlays;
+        };
     };
   };
 }
modifiedpkgs/default.nixdiffbeforeafterboth
--- a/pkgs/default.nix
+++ b/pkgs/default.nix
@@ -1,10 +1,11 @@
 {
   callPackage,
   craneLib,
-  inputs',
+  inputs,
 }:
 {
-  fleet = callPackage ./fleet.nix { inherit craneLib inputs'; };
+  fleet = callPackage ./fleet.nix { inherit craneLib inputs; };
+  remowt-plugin-fleet = callPackage ./remowt-plugin-fleet.nix { inherit craneLib inputs; };
   fleet-install-secrets = callPackage ./fleet-install-secrets.nix { inherit craneLib; };
   fleet-generator-helper = callPackage ./fleet-generator-helper.nix { inherit craneLib; };
 }
modifiedpkgs/fleet.nixdiffbeforeafterboth
--- a/pkgs/fleet.nix
+++ b/pkgs/fleet.nix
@@ -2,10 +2,16 @@
   lib,
   craneLib,
   installShellFiles,
-  inputs',
+  inputs,
+
+  stdenv,
   pkg-config,
   rustPlatform,
+  rofi,
 }:
+let
+  system = stdenv.hostPlatform.system;
+in
 craneLib.buildPackage rec {
   pname = "fleet";
   src = lib.cleanSourceWith {
@@ -20,10 +26,15 @@
 
   cargoExtraArgs = "--locked -p ${pname}";
 
+  REMOWT_AGENTS_DIR = "${inputs.remowt-agents.packages.${system}.remowt-agents}";
+  # TODO: built-in fleet prompter should be a prodash widget, or it should require
+  # tty remowt prompter running on host machine idk.
+  ROFI = "${rofi}/bin/rofi";
+
   buildInputs = [
-    inputs'.nix.packages.nix-expr-c
-    inputs'.nix.packages.nix-flake-c
-    inputs'.nix.packages.nix-fetchers-c
+    inputs.nix.packages.${system}.nix-expr-c
+    inputs.nix.packages.${system}.nix-flake-c
+    inputs.nix.packages.${system}.nix-fetchers-c
   ];
   nativeBuildInputs = [
     installShellFiles
addedpkgs/remowt-plugin-fleet.nixdiffbeforeafterboth
--- /dev/null
+++ b/pkgs/remowt-plugin-fleet.nix
@@ -0,0 +1,36 @@
+{
+  lib,
+  craneLib,
+  inputs,
+
+  stdenv,
+  pkg-config,
+  rustPlatform,
+}:
+let
+  system = stdenv.hostPlatform.system;
+in
+craneLib.buildPackage rec {
+  pname = "remowt-plugin-fleet";
+  src = lib.cleanSourceWith {
+    src = ../.;
+    filter =
+      path: type:
+      (lib.hasSuffix "\.cc" path)
+      || (lib.hasSuffix "\.hh" path)
+      || (craneLib.filterCargoSources path type);
+  };
+  strictDeps = true;
+
+  cargoExtraArgs = "--locked -p ${pname}";
+
+  buildInputs = [
+    inputs.nix.packages.${system}.nix-expr-c
+    inputs.nix.packages.${system}.nix-flake-c
+    inputs.nix.packages.${system}.nix-fetchers-c
+  ];
+  nativeBuildInputs = [
+    pkg-config
+    rustPlatform.bindgenHook
+  ];
+}