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
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
+  ];
+}