diff --git a/Cargo.lock b/Cargo.lock index c8f2a81843..ad640780da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,20 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "agent-client-protocol" version = "0.11.1" @@ -268,6 +282,47 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "askama" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" +dependencies = [ + "askama_derive", + "askama_escape", +] + +[[package]] +name = "askama_derive" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" +dependencies = [ + "askama_parser", + "basic-toml", + "mime", + "mime_guess", + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_parser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "asn1-rs" version = "0.7.2" @@ -410,6 +465,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version", +] + [[package]] name = "atoi" version = "2.0.0" @@ -431,6 +497,18 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "attohttpc" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" +dependencies = [ + "base64 0.22.1", + "http 1.4.1", + "log", + "url", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -940,7 +1018,7 @@ dependencies = [ "arc-swap", "bytes", "either", - "fs-err", + "fs-err 3.3.0", "http 1.4.1", "http-body 1.0.1", "hyper", @@ -962,6 +1040,17 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7" +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + [[package]] name = "backtrace" version = "0.3.76" @@ -983,6 +1072,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + [[package]] name = "base32" version = "0.5.1" @@ -1023,6 +1118,15 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + [[package]] name = "bat" version = "0.26.1" @@ -1826,6 +1930,29 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "castaway" version = "0.2.4" @@ -2093,8 +2220,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "961b955a666e25ee5a1091d219128d6e6401e3dab84efb1a2bf6b4035d797b39" dependencies = [ "crmf", - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", "x509-cert", ] @@ -2105,11 +2232,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b77c319abfd5219629c45c34c89ba945ed3c5e49fcde9d16b6c3885f118a730" dependencies = [ "const-oid 0.9.6", - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", "x509-cert", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -2294,6 +2430,16 @@ dependencies = [ "url", ] +[[package]] +name = "cordyceps" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" +dependencies = [ + "loom", + "tracing", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -2388,6 +2534,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crmf" version = "0.2.0" @@ -2395,8 +2547,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36fe21b96d5b87f5de4b5b7202ec41c00110ac817ce6728fe75fb2fe5962ed92" dependencies = [ "cms", - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", "x509-cert", ] @@ -2502,6 +2654,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "ctutils" version = "0.4.2" @@ -2560,12 +2721,30 @@ dependencies = [ "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest 0.10.7", - "fiat-crypto", + "fiat-crypto 0.2.9", "rustc_version", "subtle", "zeroize", ] +[[package]] +name = "curve25519-dalek" +version = "5.0.0-pre.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335f1947f241137a14106b6f5acc5918a5ede29c9d71d3f2cb1678d5075d9fc3" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.11.3", + "fiat-crypto 0.3.0", + "rand_core 0.10.1", + "rustc_version", + "serde", + "subtle", + "zeroize", +] + [[package]] name = "curve25519-dalek-derive" version = "0.1.1" @@ -2688,6 +2867,26 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +[[package]] +name = "data-encoding-macro" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3259c913752a86488b501ed8680446a5ed2d5aeac6e596cb23ba3800768ea32c" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" +dependencies = [ + "data-encoding", + "syn 2.0.117", +] + [[package]] name = "data-url" version = "0.3.2" @@ -3103,7 +3302,18 @@ dependencies = [ "const-oid 0.9.6", "der_derive", "flagset", - "pem-rfc7468", + "pem-rfc7468 0.7.0", + "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 1.0.0", "zeroize", ] @@ -3207,6 +3417,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "diatomic-waker" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" + [[package]] name = "diff" version = "0.1.13" @@ -3318,6 +3534,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags 2.11.1", + "block2", + "libc", "objc2", ] @@ -3338,6 +3556,17 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d" +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "libc", + "once_cell", + "winapi", +] + [[package]] name = "doc-comment" version = "0.3.4" @@ -3454,12 +3683,12 @@ version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der", + "der 0.7.10", "digest 0.10.7", "elliptic-curve", "rfc6979", - "signature", - "spki", + "signature 2.2.0", + "spki 0.7.3", ] [[package]] @@ -3477,8 +3706,19 @@ version = "2.2.3" 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", + "serdect", + "signature 3.0.0", ] [[package]] @@ -3487,14 +3727,30 @@ 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 0.10.9", "subtle", "zeroize", ] +[[package]] +name = "ed25519-dalek" +version = "3.0.0-pre.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20449acd54b660981ae5caa2bcb56d1fe7f25f2e37a38ec507400fab034d4bb6" +dependencies = [ + "curve25519-dalek 5.0.0-pre.6", + "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.16.0" @@ -3510,15 +3766,15 @@ version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct", + "base16ct 0.2.0", "crypto-bigint", "digest 0.10.7", "ff", "generic-array", "group", "hkdf", - "pem-rfc7468", - "pkcs8", + "pem-rfc7468 0.7.0", + "pkcs8 0.10.2", "rand_core 0.6.4", "sec1", "subtle", @@ -3534,6 +3790,18 @@ dependencies = [ "serde", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -3567,6 +3835,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "enum-assoc" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed8956bd5c1f0415200516e78ff07ec9e16415ade83c056c230d7b7ea0d55b7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "enumflags2" version = "0.7.12" @@ -3739,6 +4018,12 @@ 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 = "filetime" version = "0.2.29" @@ -3818,7 +4103,7 @@ checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", - "spin", + "spin 0.9.8", ] [[package]] @@ -3919,6 +4204,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + [[package]] name = "fs-err" version = "3.3.0" @@ -3976,6 +4270,19 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-buffered" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4421cb78ee172b6b06080093479d3c50f058e7c81b7d577bbb8d118d551d4cd5" +dependencies = [ + "cordyceps", + "diatomic-waker", + "futures-core", + "pin-project-lite", + "spin 0.10.0", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -4324,6 +4631,21 @@ dependencies = [ "seq-macro", ] +[[package]] +name = "generator" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result 0.4.1", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -4379,11 +4701,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", "wasip2", "wasip3", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", ] [[package]] @@ -4443,6 +4777,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + [[package]] name = "goose" version = "1.36.0" @@ -4476,7 +4821,7 @@ dependencies = [ "encoding_rs", "env-lock", "etcetera 0.11.0", - "fs-err", + "fs-err 3.3.0", "fs2", "futures", "goose-acp-macros", @@ -4492,13 +4837,14 @@ dependencies = [ "indexmap 2.14.0", "indoc", "insta", + "iroh", "jsonschema", "jsonwebtoken", "keyring", "libc", "llama-cpp-2", "llama-cpp-sys-2", - "lru", + "lru 0.16.4", "minijinja", "mockall", "nanoid", @@ -4515,7 +4861,7 @@ dependencies = [ "pctx_code_mode", "pem", "pkcs1", - "pkcs8", + "pkcs8 0.10.2", "process-wrap", "pulldown-cmark", "rand 0.8.6", @@ -4611,7 +4957,9 @@ dependencies = [ "futures", "goose", "goose-mcp", + "hex", "indicatif", + "iroh", "open", "rand 0.8.6", "regex", @@ -4749,6 +5097,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "goose-tunnel-ffi" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.22.1", + "futures", + "hex", + "iroh", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "tracing", + "uniffi", +] + [[package]] name = "group" version = "0.13.0" @@ -4864,6 +5229,11 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "hashlink" @@ -4901,6 +5271,83 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "hickory-net" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" +dependencies = [ + "async-trait", + "bytes", + "cfg-if", + "data-encoding", + "futures-channel", + "futures-io", + "futures-util", + "h2", + "hickory-proto", + "http 1.4.1", + "idna", + "ipnet", + "jni", + "rand 0.10.1", + "rustls", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tokio-rustls", + "tracing", + "url", +] + +[[package]] +name = "hickory-proto" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni", + "once_cell", + "prefix-trie", + "rand 0.10.1", + "ring", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-net", + "hickory-proto", + "ipconfig", + "ipnet", + "jni", + "moka", + "ndk-context", + "once_cell", + "parking_lot", + "rand 0.10.1", + "resolv-conf", + "rustls", + "smallvec", + "system-configuration", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", + "tracing", +] + [[package]] name = "hipstr" version = "0.6.0" @@ -5308,6 +5755,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "identity-hash" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfdd7caa900436d8f13b2346fe10257e0c05c1f1f9e351f4f5d57c03bd5f45da" + [[package]] name = "idna" version = "1.1.0" @@ -5335,6 +5788,26 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb" +[[package]] +name = "igd-next" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bac9a3c8278f43b4cd8463380f4a25653ac843e5b177e1d3eaf849cc9ba10d4d" +dependencies = [ + "attohttpc", + "bytes", + "futures", + "http 1.4.1", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand 0.10.1", + "tokio", + "url", + "xmltree", +] + [[package]] name = "ignore" version = "0.4.25" @@ -5506,11 +5979,199 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2", + "widestring", + "windows-registry", + "windows-result 0.4.1", + "windows-sys 0.61.2", +] + [[package]] name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +dependencies = [ + "serde", +] + +[[package]] +name = "iroh" +version = "1.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef865dc2d11a19fe670ff217b68ffc3b511bddf473dc3a3e120090b9f691803" +dependencies = [ + "backon", + "blake3", + "bytes", + "cfg_aliases", + "ctutils", + "data-encoding", + "derive_more", + "ed25519-dalek 3.0.0-pre.7", + "futures-util", + "getrandom 0.4.2", + "hickory-resolver", + "http 1.4.1", + "ipnet", + "iroh-base", + "iroh-dns", + "iroh-metrics", + "iroh-relay", + "n0-error", + "n0-future", + "n0-watcher", + "netwatch", + "noq", + "noq-proto", + "noq-udp", + "papaya", + "pin-project", + "portable-atomic", + "portmapper", + "rand 0.10.1", + "reqwest 0.13.4", + "rustc-hash 2.1.2", + "rustls", + "rustls-pki-types", + "rustls-webpki", + "serde", + "smallvec", + "strum 0.28.0", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "wasm-bindgen-futures", + "webpki-roots 1.0.7", +] + +[[package]] +name = "iroh-base" +version = "1.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af93d67701c00c504982154569192ad384738c0450ba1196930314b955100552" +dependencies = [ + "curve25519-dalek 5.0.0-pre.6", + "data-encoding", + "data-encoding-macro", + "derive_more", + "digest 0.11.3", + "ed25519-dalek 3.0.0-pre.7", + "getrandom 0.4.2", + "n0-error", + "rand 0.10.1", + "serde", + "sha2 0.11.0", + "url", + "zeroize", + "zeroize_derive", +] + +[[package]] +name = "iroh-dns" +version = "1.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de4112c91eb64094d77df9d3112606dcf7ff216421afccd2dc762fda5a7b2879" +dependencies = [ + "arc-swap", + "cfg_aliases", + "derive_more", + "hickory-resolver", + "iroh-base", + "n0-error", + "n0-future", + "ndk-context", + "rand 0.10.1", + "reqwest 0.13.4", + "rustls", + "simple-dns", + "strum 0.28.0", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "iroh-metrics" +version = "1.0.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d102597d0ee523f17fdb672c532395e634dbe945429284c811430d63bacc0d8a" +dependencies = [ + "iroh-metrics-derive", + "itoa", + "n0-error", + "portable-atomic", + "ryu", + "serde", + "tracing", +] + +[[package]] +name = "iroh-metrics-derive" +version = "1.0.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c8e0c97f1dc787107f388433c349397c565572fe6406d600ff7bb7b7fe3b30" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "iroh-relay" +version = "1.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70030b9e71c1183bd4f88fbdbebfa1af2a5be549dd6f20a1e8ac3cd0202ee9d" +dependencies = [ + "blake3", + "bytes", + "cfg_aliases", + "data-encoding", + "derive_more", + "getrandom 0.4.2", + "hickory-resolver", + "http 1.4.1", + "http-body-util", + "hyper", + "hyper-util", + "iroh-base", + "iroh-dns", + "iroh-metrics", + "lru 0.18.0", + "n0-error", + "n0-future", + "noq", + "noq-proto", + "num_enum", + "pin-project", + "postcard", + "rand 0.10.1", + "reqwest 0.13.4", + "rustls", + "rustls-pki-types", + "serde", + "serde_bytes", + "strum 0.28.0", + "tokio", + "tokio-rustls", + "tokio-util", + "tokio-websockets", + "tracing", + "url", + "vergen-gitcl", + "webpki-roots 1.0.7", + "ws_stream_wasm", +] [[package]] name = "is-docker" @@ -5753,7 +6414,7 @@ checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc" dependencies = [ "aws-lc-rs", "base64 0.22.1", - "ed25519-dalek", + "ed25519-dalek 2.2.0", "getrandom 0.2.17", "hmac 0.12.1", "js-sys", @@ -5765,7 +6426,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", - "signature", + "signature 2.2.0", "simple_asn1", "zeroize", ] @@ -5816,7 +6477,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin", + "spin 0.9.8", ] [[package]] @@ -5985,6 +6646,19 @@ version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + [[package]] name = "lopdf" version = "0.40.0" @@ -6019,12 +6693,27 @@ version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +[[package]] +name = "lru" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" +dependencies = [ + "hashbrown 0.17.1", +] + [[package]] name = "lru-slab" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mac-addr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d25b0e0b648a86960ac23b7ad4abb9717601dec6f66c165f5b037f3f03065f" + [[package]] name = "macro_rules_attribute" version = "0.2.2" @@ -6200,6 +6889,23 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "moka" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + [[package]] name = "monch" version = "0.5.0" @@ -6238,6 +6944,59 @@ dependencies = [ "pxfm", ] +[[package]] +name = "n0-error" +version = "1.0.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223e946a84aa91644507a6b7865cfebbb9a231ace499041c747ab0fd30408212" +dependencies = [ + "n0-error-macros", + "spez", +] + +[[package]] +name = "n0-error-macros" +version = "1.0.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "565305a21e6b3bf26640ad98f05a0fda12d3ab4315394566b52a7bddb8b34828" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "n0-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ab99dfb861450e68853d34ae665243a88b8c493d01ba957321a1e9b2312bbe" +dependencies = [ + "cfg_aliases", + "derive_more", + "futures-buffered", + "futures-lite", + "futures-util", + "js-sys", + "pin-project", + "send_wrapper", + "tokio", + "tokio-util", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "n0-watcher" +version = "1.0.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928d8039a66cce5efcfd35e88b32d3defc8eba630b3ac451522997f563956a52" +dependencies = [ + "derive_more", + "n0-error", + "n0-future", +] + [[package]] name = "nanoid" version = "0.5.0" @@ -6276,6 +7035,126 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d" +[[package]] +name = "netdev" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bacaf873ee4eab5646f99b381b271ec75e716902a67cf962c0f328c5eb5bfb" +dependencies = [ + "block2", + "dispatch2", + "dlopen2", + "ipnet", + "libc", + "mac-addr", + "netlink-packet-core", + "netlink-packet-route 0.29.0", + "netlink-sys", + "objc2-core-foundation", + "objc2-core-wlan", + "objc2-foundation", + "objc2-system-configuration", + "once_cell", + "plist", + "windows-sys 0.61.2", +] + +[[package]] +name = "netlink-packet-core" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-packet-route" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9854ea6ad14e3f4698a7f03b65bce0833dd2d81d594a0e4a984170537146b6" +dependencies = [ + "bitflags 2.11.1", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-packet-route" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8919612f6028ab4eacbbfe1234a9a43e3722c6e0915e7ff519066991905092" +dependencies = [ + "bitflags 2.11.1", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-proto" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.18", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +dependencies = [ + "bytes", + "futures-util", + "libc", + "log", + "tokio", +] + +[[package]] +name = "netwatch" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2071e0c2b5b229622c459096b84f1ad51afa150cdeeefdad491ef3704e581d91" +dependencies = [ + "atomic-waker", + "bytes", + "cfg_aliases", + "derive_more", + "js-sys", + "libc", + "n0-error", + "n0-future", + "n0-watcher", + "netdev", + "netlink-packet-core", + "netlink-packet-route 0.30.0", + "netlink-proto", + "netlink-sys", + "noq-udp", + "objc2-core-foundation", + "objc2-system-configuration", + "pin-project-lite", + "serde", + "socket2", + "time", + "tokio", + "tokio-util", + "tracing", + "web-sys", + "windows 0.62.2", + "windows-result 0.4.1", + "wmi", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -6364,6 +7243,68 @@ dependencies = [ "nom 8.0.0", ] +[[package]] +name = "noq" +version = "1.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "198b99fc085a5db1f7d259edb5ede8311e59f28cdd2687920b4313613d21a73f" +dependencies = [ + "bytes", + "cfg_aliases", + "derive_more", + "noq-proto", + "noq-udp", + "pin-project-lite", + "rustc-hash 2.1.2", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "web-time", +] + +[[package]] +name = "noq-proto" +version = "1.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab0ac774795ce1e42a7e61266e71f3be8110210630441169ac8dda403dd23f1" +dependencies = [ + "aes-gcm", + "bytes", + "derive_more", + "enum-assoc", + "getrandom 0.4.2", + "identity-hash", + "lru-slab", + "rand 0.10.1", + "rand_pcg", + "ring", + "rustc-hash 2.1.2", + "rustls", + "rustls-pki-types", + "slab", + "sorted-index-buffer", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "noq-udp" +version = "1.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3c1520eacd33fd6b009e2e70116b05508ade51db5e0d315ff8bf6b702148c2b" +dependencies = [ + "cfg_aliases", + "libc", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + [[package]] name = "nostr" version = "0.44.3" @@ -6394,7 +7335,7 @@ version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1" dependencies = [ - "lru", + "lru 0.16.4", "nostr", "tokio", ] @@ -6418,7 +7359,7 @@ dependencies = [ "async-wsocket", "atomic-destructor", "hex", - "lru", + "lru 0.16.4", "negentropy", "nostr", "nostr-database", @@ -6591,6 +7532,37 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "oauth2" version = "5.0.0" @@ -6648,7 +7620,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.11.1", + "block2", "dispatch2", + "libc", "objc2", ] @@ -6665,6 +7639,20 @@ dependencies = [ "objc2-io-surface", ] +[[package]] +name = "objc2-core-wlan" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e34919aba0d701380d911702455038a8a3587467fe0141d6a71501e7ffe48" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-security", + "objc2-security-foundation", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -6709,6 +7697,41 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-security-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef76382e9cedd18123099f17638715cc3d81dba3637d4c0d39ab69df2ef345a5" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "libc", + "objc2", + "objc2-core-foundation", + "objc2-security", +] + [[package]] name = "object" version = "0.37.3" @@ -6732,6 +7755,10 @@ name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "once_cell_polyfill" @@ -7035,6 +8062,16 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "papaya" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "997ee03cd38c01469a7046643714f0ad28880bcb9e6679ff0666e24817ca19b7" +dependencies = [ + "equivalent", + "seize", +] + [[package]] name = "par-core" version = "2.0.0" @@ -7299,6 +8336,15 @@ dependencies = [ "base64ct", ] +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -7348,6 +8394,16 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version", +] + [[package]] name = "phf" version = "0.11.3" @@ -7446,9 +8502,9 @@ 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]] @@ -7457,8 +8513,18 @@ 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", + "spki 0.8.0", ] [[package]] @@ -7523,11 +8589,26 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +dependencies = [ + "serde", +] [[package]] name = "portable-atomic-util" @@ -7538,6 +8619,59 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "portmapper" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64959cbabf952c8ffcbaea13745308508f1f825922f4068353f3de08d42cf214" +dependencies = [ + "base64 0.22.1", + "bytes", + "derive_more", + "hyper-util", + "igd-next", + "iroh-metrics", + "libc", + "n0-error", + "n0-future", + "netwatch", + "num_enum", + "rand 0.10.1", + "serde", + "smallvec", + "socket2", + "time", + "tokio", + "tokio-util", + "tower-layer", + "tracing", + "url", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "postcard-derive", + "serde", +] + +[[package]] +name = "postcard-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0232bd009a197ceec9cc881ba46f727fcd8060a2d8d6a9dde7a69030a6fe2bb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -7590,6 +8724,17 @@ dependencies = [ "termtree", ] +[[package]] +name = "prefix-trie" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -7619,6 +8764,15 @@ 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" version = "1.0.4" @@ -7983,6 +9137,15 @@ dependencies = [ "rand 0.9.4", ] +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "rangemap" version = "1.7.1" @@ -8262,6 +9425,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + [[package]] name = "rfc6979" version = "0.4.0" @@ -8363,10 +9532,10 @@ dependencies = [ "num-integer", "num-traits", "pkcs1", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", - "signature", - "spki", + "signature 2.2.0", + "spki 0.7.3", "subtle", "zeroize", ] @@ -8450,6 +9619,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", + "log", "once_cell", "ring", "rustls-pki-types", @@ -8690,6 +9860,26 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "scrypt" version = "0.11.0" @@ -8714,10 +9904,10 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct", - "der", + "base16ct 0.2.0", + "der 0.7.10", "generic-array", - "pkcs8", + "pkcs8 0.10.2", "subtle", "zeroize", ] @@ -8778,11 +9968,31 @@ dependencies = [ "libc", ] +[[package]] +name = "seize" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "seq-macro" @@ -8966,6 +10176,16 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct 1.0.0", + "serde", +] + [[package]] name = "serial_test" version = "3.4.0" @@ -9000,6 +10220,12 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -9072,6 +10298,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" + [[package]] name = "sigstore-bundle" version = "0.8.0" @@ -9099,14 +10331,14 @@ dependencies = [ "aws-lc-rs", "base64 0.22.1", "const-oid 0.9.6", - "der", + "der 0.7.10", "digest 0.10.7", "pem", "rand_core 0.9.5", "sha2 0.10.9", - "signature", + "signature 2.2.0", "sigstore-types", - "spki", + "spki 0.7.3", "thiserror 2.0.18", "tracing", "x509-cert", @@ -9172,7 +10404,7 @@ dependencies = [ "cmpv2", "cms", "const-oid 0.9.6", - "der", + "der 0.7.10", "hex", "jiff", "rand 0.9.4", @@ -9263,6 +10495,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "simple-dns" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a75cbde1bf934313596a004973e462f9a82caa814dcf1a5f507bdf51597eeb4" +dependencies = [ + "bitflags 2.11.1", +] + [[package]] name = "simple_asn1" version = "0.6.4" @@ -9342,6 +10583,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "sorted-index-buffer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea06cc588e43c632923a55450401b8f25e628131571d4e1baea1bdfdb2b5ed06" + [[package]] name = "sourcemap" version = "9.3.2" @@ -9360,6 +10607,17 @@ dependencies = [ "url", ] +[[package]] +name = "spez" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87e960f4dca2788eeb86bbdde8dd246be8948790b7618d656e68f9b720a86e8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "spin" version = "0.9.8" @@ -9369,6 +10627,12 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + [[package]] name = "spki" version = "0.7.3" @@ -9376,7 +10640,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" 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]] @@ -10413,6 +11687,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tap" version = "1.0.1" @@ -10678,7 +11958,10 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "js-sys", + "libc", "num-conv", + "num_threads", "powerfmt", "serde_core", "time-core", @@ -10922,6 +12205,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -10973,6 +12257,38 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-websockets" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad543404f98bfc969aeb71994105c592acfc6c43323fddcd016bb208d1c65cb" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-sink", + "getrandom 0.4.2", + "http 1.4.1", + "httparse", + "rand 0.10.1", + "ring", + "rustls-pki-types", + "sha1_smol", + "simdutf8", + "tokio", + "tokio-rustls", + "tokio-util", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.9.12+spec-1.1.0" @@ -11019,6 +12335,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + [[package]] name = "toml_parser" version = "1.1.2+spec-1.1.0" @@ -11186,6 +12514,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + [[package]] name = "tracing-opentelemetry" version = "0.33.0" @@ -11224,10 +12563,12 @@ dependencies = [ "serde", "serde_json", "sharded-slab", + "smallvec", "thread_local", "time", "tracing", "tracing-core", + "tracing-log", "tracing-serde", ] @@ -11592,6 +12933,124 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "uniffi" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cb08c58c7ed7033150132febe696bef553f891b1ede57424b40d87a89e3c170" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "clap", + "uniffi_bindgen", + "uniffi_core", + "uniffi_macros", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cade167af943e189a55020eda2c314681e223f1e42aca7c4e52614c2b627698f" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err 2.11.0", + "glob", + "goblin", + "heck", + "once_cell", + "paste", + "serde", + "textwrap", + "toml 0.5.11", + "uniffi_meta", + "uniffi_udl", +] + +[[package]] +name = "uniffi_checksum_derive" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "802d2051a700e3ec894c79f80d2705b69d85844dafbbe5d1a92776f8f48b563a" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "uniffi_core" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7687007d2546c454d8ae609b105daceb88175477dac280707ad6d95bcd6f1f" +dependencies = [ + "anyhow", + "bytes", + "log", + "once_cell", + "paste", + "static_assertions", +] + +[[package]] +name = "uniffi_macros" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12c65a5b12ec544ef136693af8759fb9d11aefce740fb76916721e876639033b" +dependencies = [ + "bincode", + "camino", + "fs-err 2.11.0", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "toml 0.5.11", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a74ed96c26882dac1ca9b93ca23c827e284bacbd7ec23c6f0b0372f747d59e4" +dependencies = [ + "anyhow", + "bytes", + "siphasher 0.3.11", + "uniffi_checksum_derive", +] + +[[package]] +name = "uniffi_testing" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6f984f0781f892cc864a62c3a5c60361b1ccbd68e538e6c9fbced5d82268ac" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "fs-err 2.11.0", + "once_cell", +] + +[[package]] +name = "uniffi_udl" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037820a4cfc4422db1eaa82f291a3863c92c7d1789dc513489c36223f9b4cdfc" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "uniffi_testing", + "weedle2", +] + [[package]] name = "unit-prefix" version = "0.5.2" @@ -11784,6 +13243,43 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vergen" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b849a1f6d8639e8de261e81ee0fc881e3e3620db1af9f2e0da015d4382ceaf75" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", + "vergen-lib", +] + +[[package]] +name = "vergen-gitcl" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ff3b5300a085d6bcd8fc96a507f706a28ae3814693236c9b409db71a1d15b9" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", + "time", + "vergen", + "vergen-lib", +] + +[[package]] +name = "vergen-lib" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b34a29ba7e9c59e62f229ae1932fb1b8fb8a6fdcc99215a641913f5f5a59a569" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", +] + [[package]] name = "version_check" version = "0.9.5" @@ -12020,6 +13516,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "weezl" version = "0.1.12" @@ -12069,6 +13574,12 @@ dependencies = [ "wasite", ] +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -12653,12 +14164,46 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wmi" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c81b85c57a57500e56669586496bf2abd5cf082b9d32995251185d105208b64" +dependencies = [ + "chrono", + "futures", + "log", + "serde", + "thiserror 2.0.18", + "windows 0.62.2", + "windows-core 0.62.2", +] + [[package]] name = "writeable" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "ws_stream_wasm" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version", + "send_wrapper", + "thiserror 2.0.18", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wyz" version = "0.5.1" @@ -12692,10 +14237,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" dependencies = [ "const-oid 0.9.6", - "der", + "der 0.7.10", "sha1", - "signature", - "spki", + "signature 2.2.0", + "spki 0.7.3", "tls_codec", ] @@ -12725,15 +14270,30 @@ checksum = "f5ceece934a21607055b7ac5c25adb56a2ff559804b10705dc674d1d838c15e1" dependencies = [ "cmpv2", "cms", - "der", + "der 0.7.10", ] +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + [[package]] name = "xmlparser" version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + [[package]] name = "xterm-color" version = "1.0.2" diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index 7dadbd32c8..dc8f760975 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -64,6 +64,8 @@ sha2 = { workspace = true } sigstore-verify = { version = "0.8", default-features = false, optional = true } axum.workspace = true clap_complete_nushell = { version = "4", default-features = false } +iroh = "1.0.0-rc.1" +hex = "0.4.3" [target.'cfg(target_os = "windows")'.dependencies] anstream = { version = "1", default-features = false, features = ["wincon"] } diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index aac8b77c3b..b2a2bfcee4 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -831,6 +831,10 @@ enum Command { action = clap::ArgAction::Append )] builtins: Vec, + + /// Serve ACP over an iroh QUIC transport (relay + direct) instead of HTTP + #[arg(long)] + iroh: bool, }, /// Start or resume interactive chat sessions @@ -1368,6 +1372,60 @@ async fn handle_serve_command(host: String, port: u16, builtins: Vec) -> Ok(()) } +async fn handle_serve_iroh_command(builtins: Vec) -> Result<()> { + use goose::acp::server_factory::{AcpServer, AcpServerFactoryConfig}; + use goose::acp::transport::iroh as iroh_transport; + use goose::config::paths::Paths; + use std::sync::Arc; + use tracing::info; + + let builtins = if builtins.is_empty() { + vec!["developer".to_string()] + } else { + builtins + }; + + let server = Arc::new(AcpServer::new(AcpServerFactoryConfig { + builtins, + data_dir: Paths::data_dir(), + config_dir: Paths::config_dir(), + goose_platform: GoosePlatform::GooseCli, + additional_source_roots: Vec::new(), + })); + + // Persist a stable NodeId across restarts so the paired client keeps working. + let secret_key = load_or_create_iroh_secret_key()?; + let (endpoint, token) = iroh_transport::bind_server(secret_key).await?; + + info!("iroh ACP server ready"); + println!("\n=== goose iroh ACP server ==="); + println!("NodeId: {}", endpoint.id()); + println!("Connection token (QR payload):\n{token}\n"); + + iroh_transport::serve(endpoint, server).await +} + +fn load_or_create_iroh_secret_key() -> Result { + use goose::config::paths::Paths; + use std::io::Write as _; + + let path = Paths::config_dir().join("iroh_acp_secret.key"); + if let Ok(hex_str) = std::fs::read_to_string(&path) { + if let Ok(bytes) = hex::decode(hex_str.trim()) { + if let Ok(arr) = <[u8; 32]>::try_from(bytes.as_slice()) { + return Ok(iroh::SecretKey::from_bytes(&arr)); + } + } + } + let key = iroh::SecretKey::generate(); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let mut f = std::fs::File::create(&path)?; + write!(f, "{}", hex::encode(key.to_bytes()))?; + Ok(key) +} + async fn handle_session_subcommand(command: SessionCommand) -> Result<()> { match command { SessionCommand::List { @@ -2091,7 +2149,14 @@ pub async fn cli() -> anyhow::Result<()> { host, port, builtins, - }) => handle_serve_command(host, port, builtins).await, + iroh, + }) => { + if iroh { + handle_serve_iroh_command(builtins).await + } else { + handle_serve_command(host, port, builtins).await + } + } Some(Command::Session { command: Some(cmd), .. }) => handle_session_subcommand(cmd).await, diff --git a/crates/goose-tunnel-ffi/Cargo.toml b/crates/goose-tunnel-ffi/Cargo.toml new file mode 100644 index 0000000000..826ebcb3ad --- /dev/null +++ b/crates/goose-tunnel-ffi/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "goose-tunnel-ffi" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "goose_tunnel_ffi" +crate-type = ["staticlib", "cdylib", "lib"] + +[[bin]] +name = "uniffi-bindgen" +path = "src/uniffi-bindgen.rs" + +[dependencies] +iroh = "1.0.0-rc.1" +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "io-util", "sync", "time"] } +tokio-util = { workspace = true, features = ["compat"] } +futures = { workspace = true } +anyhow = { workspace = true } +base64 = { workspace = true } +serde_json = { workspace = true } +uniffi = { version = "0.28", features = ["cli"] } +tracing = { workspace = true } +thiserror = { workspace = true } +hex = "0.4" diff --git a/crates/goose-tunnel-ffi/src/lib.rs b/crates/goose-tunnel-ffi/src/lib.rs new file mode 100644 index 0000000000..e955e87087 --- /dev/null +++ b/crates/goose-tunnel-ffi/src/lib.rs @@ -0,0 +1,226 @@ +//! goose-owned FFI for the iroh ACP tunnel. +//! +//! Exposes a small, goose-shaped surface to Swift (and Kotlin) over UniFFI: +//! connect to a paired goosed over iroh QUIC, open one ACP bidi stream, send +//! newline-delimited JSON-RPC lines, and receive inbound lines via a callback. +//! +//! ACP itself lives in Swift β€” this crate is only the authenticated byte pipe +//! (iroh: relay + direct path + NodeId identity). No HTTP, no Rust ACP logic. + +use std::sync::Arc; + +use base64::Engine as _; +use iroh::{endpoint::Endpoint, RelayConfig, RelayMap, RelayMode, RelayUrl, SecretKey}; +use tokio::io::AsyncWriteExt; +use tokio::sync::{mpsc, Mutex}; + +uniffi::setup_scaffolding!(); + +const ALPN_GOOSE_ACP_V1: &[u8] = b"goose-acp/1"; + +const DEFAULT_RELAYS: &[&str] = &[ + "https://usw1-2.relay.michaelneale.mesh-llm.iroh.link./", + "https://aps1-1.relay.michaelneale.mesh-llm.iroh.link./", +]; + +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum TunnelError { + #[error("invalid connection token: {0}")] + InvalidToken(String), + #[error("connection failed: {0}")] + ConnectFailed(String), + #[error("transport error: {0}")] + Transport(String), +} + +/// Whether traffic is flowing direct (hole-punched) or via a relay. +#[derive(Debug, Clone, Copy, uniffi::Enum)] +pub enum PathKind { + Connecting, + Direct, + Relayed, +} + +/// Implemented in Swift; receives each inbound newline-delimited JSON-RPC line. +#[uniffi::export(with_foreign)] +pub trait MessageListener: Send + Sync { + fn on_message(&self, line: String); + fn on_closed(&self, reason: String); +} + +/// Generate a device keypair (hex-encoded 32-byte secret). The NodeId is the +/// public key; persist this in the Keychain so the device identity is stable. +#[uniffi::export] +pub fn generate_device_keypair() -> String { + let key = SecretKey::generate(); + hex::encode(key.to_bytes()) +} + +fn relay_mode() -> RelayMode { + let configs = DEFAULT_RELAYS + .iter() + .filter_map(|u| u.parse::().ok()) + .map(|url| RelayConfig::new(url, None)); + RelayMode::Custom(RelayMap::from_iter(configs)) +} + +fn decode_addr(token: &str) -> Result { + let json = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(token) + .map_err(|e| TunnelError::InvalidToken(e.to_string()))?; + serde_json::from_slice(&json).map_err(|e| TunnelError::InvalidToken(e.to_string())) +} + +fn device_key(hex_key: &str) -> Result { + let bytes = hex::decode(hex_key).map_err(|e| TunnelError::InvalidToken(e.to_string()))?; + let arr: [u8; 32] = bytes + .as_slice() + .try_into() + .map_err(|_| TunnelError::InvalidToken("device key must be 32 bytes".into()))?; + Ok(SecretKey::from_bytes(&arr)) +} + +/// A connected ACP tunnel: one iroh QUIC bidi stream carrying JSON-RPC lines. +#[derive(uniffi::Object)] +pub struct GooseTunnel { + runtime: Arc, + send_tx: mpsc::UnboundedSender, + endpoint: Endpoint, + server_addr: iroh::EndpointAddr, + closed: Arc>, +} + +#[uniffi::export] +impl GooseTunnel { + /// Send one newline-delimited JSON-RPC line to the agent. + pub fn send(&self, line: String) -> Result<(), TunnelError> { + self.send_tx + .send(line) + .map_err(|e| TunnelError::Transport(e.to_string())) + } + + /// Direct vs relayed, observed from iroh's path info. + pub fn path_kind(&self) -> PathKind { + let info = self + .runtime + .block_on(async { self.endpoint.remote_info(self.server_addr.id).await }); + match info { + Some(info) => { + let has_direct = info + .addrs() + .any(|a| matches!(a.addr(), iroh::TransportAddr::Ip(_))); + if has_direct { + PathKind::Direct + } else { + PathKind::Relayed + } + } + None => PathKind::Connecting, + } + } + + pub fn disconnect(&self) { + self.runtime.block_on(async { + *self.closed.lock().await = true; + self.endpoint.close().await; + }); + } +} + +/// Connect to a paired goosed over iroh and open one ACP stream. +/// +/// `server_token` is the base64url `EndpointAddr` from the QR. `device_key_hex` +/// is this device's persisted keypair. Inbound lines are delivered to `listener`. +#[uniffi::export] +pub fn connect( + server_token: String, + device_key_hex: String, + listener: Arc, +) -> Result, TunnelError> { + let runtime = Arc::new( + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .map_err(|e| TunnelError::Transport(e.to_string()))?, + ); + + let server_addr = decode_addr(&server_token)?; + let secret = device_key(&device_key_hex)?; + let (send_tx, mut send_rx) = mpsc::unbounded_channel::(); + let closed = Arc::new(Mutex::new(false)); + + let endpoint = runtime + .block_on(async { + Endpoint::builder(iroh::endpoint::presets::Minimal) + .secret_key(secret) + .alpns(vec![ALPN_GOOSE_ACP_V1.to_vec()]) + .relay_mode(relay_mode()) + .bind() + .await + }) + .map_err(|e| TunnelError::ConnectFailed(e.to_string()))?; + + // Connect and open the bidi stream. + let (mut send, mut recv) = runtime + .block_on(async { + let _ = + tokio::time::timeout(std::time::Duration::from_secs(10), endpoint.online()).await; + let conn = endpoint + .connect(server_addr.clone(), ALPN_GOOSE_ACP_V1) + .await + .map_err(|e| anyhow::anyhow!(e.to_string()))?; + conn.open_bi() + .await + .map_err(|e| anyhow::anyhow!(e.to_string())) + }) + .map_err(|e: anyhow::Error| TunnelError::ConnectFailed(e.to_string()))?; + + let closed_writer = closed.clone(); + runtime.spawn(async move { + while let Some(line) = send_rx.recv().await { + if *closed_writer.lock().await { + break; + } + let mut buf = line.into_bytes(); + buf.push(b'\n'); + if send.write_all(&buf).await.is_err() { + break; + } + let _ = send.flush().await; + } + }); + + let listener_reader = listener.clone(); + let closed_reader = closed.clone(); + runtime.spawn(async move { + let mut buf = Vec::with_capacity(8192); + let mut chunk = [0u8; 4096]; + loop { + match recv.read(&mut chunk).await { + Ok(None) | Ok(Some(0)) | Err(_) => { + listener_reader.on_closed("stream ended".into()); + *closed_reader.lock().await = true; + break; + } + Ok(Some(n)) => { + buf.extend_from_slice(&chunk[..n]); + while let Some(pos) = buf.iter().position(|&b| b == b'\n') { + let line: Vec = buf.drain(..=pos).collect(); + let line = String::from_utf8_lossy(&line[..line.len() - 1]).to_string(); + if !line.is_empty() { + listener_reader.on_message(line); + } + } + } + } + } + }); + + Ok(Arc::new(GooseTunnel { + runtime, + send_tx, + endpoint, + server_addr, + closed, + })) +} diff --git a/crates/goose-tunnel-ffi/src/uniffi-bindgen.rs b/crates/goose-tunnel-ffi/src/uniffi-bindgen.rs new file mode 100644 index 0000000000..f6cff6cf1d --- /dev/null +++ b/crates/goose-tunnel-ffi/src/uniffi-bindgen.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi::uniffi_bindgen_main() +} diff --git a/crates/goose-tunnel-ffi/swift-driver/main.swift b/crates/goose-tunnel-ffi/swift-driver/main.swift new file mode 100644 index 0000000000..4d1694741a --- /dev/null +++ b/crates/goose-tunnel-ffi/swift-driver/main.swift @@ -0,0 +1,66 @@ +import Foundation + +// End-to-end Swift driver: connect to goosed over iroh, speak ACP (newline JSON-RPC). +// Proves: pure Swift -> goose-owned FFI -> iroh QUIC (relay/direct) -> goosed ACP. +// +// Build/run via crates/goose-tunnel-ffi/swift-driver/run.sh + +final class ACPListener: MessageListener { + let sema: DispatchSemaphore + var sawInitialize = false + init(_ sema: DispatchSemaphore) { self.sema = sema } + + func onMessage(line: String) { + print("⬅️ \(line)") + if let data = line.data(using: .utf8), + let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + obj["result"] != nil, obj["id"] as? Int == 1 { + sawInitialize = true + print("βœ… ACP initialize round-trip succeeded over iroh") + sema.signal() + } + } + + func onClosed(reason: String) { + print("πŸ”Œ stream closed: \(reason)") + sema.signal() + } +} + +guard CommandLine.arguments.count >= 2 else { + FileHandle.standardError.write("usage: driver \n".data(using: .utf8)!) + exit(2) +} +let token = CommandLine.arguments[1] + +let deviceKey = generateDeviceKeypair() +print("πŸ”‘ device key (NodeId pinned on pairing): \(deviceKey.prefix(16))…") + +let done = DispatchSemaphore(value: 0) +let listener = ACPListener(done) + +do { + print("🌐 connecting over iroh…") + let tunnel = try connect(serverToken: token, deviceKeyHex: deviceKey, listener: listener) + print("πŸ›£οΈ path: \(tunnel.pathKind())") + + let initReq: [String: Any] = [ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": ["protocolVersion": 1], + ] + let data = try JSONSerialization.data(withJSONObject: initReq) + let line = String(data: data, encoding: .utf8)! + print("➑️ \(line)") + try tunnel.send(line: line) + + let result = done.wait(timeout: .now() + 30) + if result == .timedOut { print("⏱️ timed out waiting for ACP response") } + print("πŸ›£οΈ final path: \(tunnel.pathKind())") + tunnel.disconnect() + exit(listener.sawInitialize ? 0 : 1) +} catch { + print("❌ \(error)") + exit(1) +} diff --git a/crates/goose-tunnel-ffi/swift-driver/mobile-lifecycle.swift b/crates/goose-tunnel-ffi/swift-driver/mobile-lifecycle.swift new file mode 100644 index 0000000000..eea416bc6b --- /dev/null +++ b/crates/goose-tunnel-ffi/swift-driver/mobile-lifecycle.swift @@ -0,0 +1,84 @@ +import Foundation + +// Mobile lifecycle harness: exercises the iOS suspend/resume cycle against a real +// goosed iroh server, the way the mobile app would actually behave. +// +// foreground -> connect -> ACP initialize +// background -> disconnect (iOS freezes the process; UDP socket dies) +// foreground -> reconnect (fresh tunnel) -> ACP initialize again [x3] +// +// This is the part a plain CLI run does NOT cover: connection teardown on +// suspend and fast re-establishment on resume, repeatedly, over real QUIC/relay. + +final class CycleListener: MessageListener { + private let sema: DispatchSemaphore + private(set) var gotInit = false + init(_ s: DispatchSemaphore) { sema = s } + func onMessage(line: String) { + if let d = line.data(using: .utf8), + let o = try? JSONSerialization.jsonObject(with: d) as? [String: Any], + o["result"] != nil, (o["id"] as? Int) == 1 { + gotInit = true + sema.signal() + } + } + func onClosed(reason: String) { /* expected on background teardown */ } +} + +@main +struct MobileLifecycle { + static func foregroundConnectAndInit(token: String, deviceKey: String) -> (ok: Bool, path: String, ms: Int) { + let sema = DispatchSemaphore(value: 0) + let listener = CycleListener(sema) + let start = Date() + do { + let tunnel = try connect(serverToken: token, deviceKeyHex: deviceKey, listener: listener) + let initReq: [String: Any] = ["jsonrpc": "2.0", "id": 1, "method": "initialize", + "params": ["protocolVersion": 1]] + let line = String(data: try JSONSerialization.data(withJSONObject: initReq), encoding: .utf8)! + try tunnel.send(line: line) + let waited = sema.wait(timeout: .now() + 20) + let ms = Int(Date().timeIntervalSince(start) * 1000) + let path = "\(tunnel.pathKind())" + // Simulate iOS backgrounding: clean-close so we don't leave a zombie. + tunnel.disconnect() + return (waited == .success && listener.gotInit, path, ms) + } catch { + return (false, "error: \(error)", Int(Date().timeIntervalSince(start) * 1000)) + } + } + + static func main() { + guard CommandLine.arguments.count >= 2 else { + FileHandle.standardError.write("usage: mobile-lifecycle \n".data(using: .utf8)!) + exit(2) + } + let token = CommandLine.arguments[1] + + // The device identity is generated once and persisted (Keychain on a real + // phone). Reused across every foreground/reconnect so the server sees a + // stable NodeId. + let deviceKey = generateDeviceKeypair() + print("πŸ“± device identity (stable across suspend/resume): \(deviceKey.prefix(16))…") + + var allOk = true + let cycles = 3 + for cycle in 1...cycles { + print("\nπŸ”† FOREGROUND #\(cycle): connecting over iroh…") + let r = foregroundConnectAndInit(token: token, deviceKey: deviceKey) + if r.ok { + print(" βœ… ACP initialize ok | path: \(r.path) | \(r.ms)ms") + } else { + print(" ❌ failed | \(r.path) | \(r.ms)ms") + allOk = false + } + if cycle < cycles { + print("πŸŒ™ BACKGROUND: app suspended, tunnel torn down (UDP frozen on real iOS)") + Thread.sleep(forTimeInterval: 2.0) + } + } + + print("\n\(allOk ? "βœ… PASS" : "❌ FAIL"): survived \(cycles) suspend/resume cycles with stable device identity") + exit(allOk ? 0 : 1) + } +} diff --git a/crates/goose-tunnel-ffi/swift-driver/path-probe.swift b/crates/goose-tunnel-ffi/swift-driver/path-probe.swift new file mode 100644 index 0000000000..16ed7e5261 --- /dev/null +++ b/crates/goose-tunnel-ffi/swift-driver/path-probe.swift @@ -0,0 +1,49 @@ +import Foundation + +// Path probe: connect with a relay-only token (the off-network mobile case) and +// sample path_kind() over time to observe iroh's relay -> direct upgrade. +// +// On a single host a loopback direct path always exists, so iroh correctly +// upgrades relay -> direct after the disco handshake. This probe documents that +// progression honestly rather than pretending the relay path is permanent. + +final class Probe: MessageListener { + let sema = DispatchSemaphore(value: 0) + var gotInit = false + func onMessage(line: String) { + if let d = line.data(using: .utf8), + let o = try? JSONSerialization.jsonObject(with: d) as? [String: Any], + o["result"] != nil, (o["id"] as? Int) == 1 { gotInit = true; sema.signal() } + } + func onClosed(reason: String) {} +} + +@main +struct PathProbe { + static func main() { + guard CommandLine.arguments.count >= 2 else { + FileHandle.standardError.write("usage: path-probe \n".data(using: .utf8)!) + exit(2) + } + let token = CommandLine.arguments[1] + let deviceKey = generateDeviceKeypair() + let probe = Probe() + do { + let tunnel = try connect(serverToken: token, deviceKeyHex: deviceKey, listener: probe) + print("t=0.0s path: \(tunnel.pathKind()) (just connected)") + let initReq: [String: Any] = ["jsonrpc": "2.0", "id": 1, "method": "initialize", + "params": ["protocolVersion": 1]] + try tunnel.send(line: String(data: try JSONSerialization.data(withJSONObject: initReq), encoding: .utf8)!) + _ = probe.sema.wait(timeout: .now() + 20) + print(" ACP initialize: \(probe.gotInit ? "ok" : "FAILED")") + for t in [1.0, 2.0, 4.0] { + Thread.sleep(forTimeInterval: t == 1.0 ? 1.0 : (t == 2.0 ? 1.0 : 2.0)) + print("t=\(t)s path: \(tunnel.pathKind())") + } + tunnel.disconnect() + exit(probe.gotInit ? 0 : 1) + } catch { + print("❌ \(error)"); exit(1) + } + } +} diff --git a/crates/goose-tunnel-ffi/swift-driver/run-mobile.sh b/crates/goose-tunnel-ffi/swift-driver/run-mobile.sh new file mode 100755 index 0000000000..241190e2f9 --- /dev/null +++ b/crates/goose-tunnel-ffi/swift-driver/run-mobile.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Build + run the mobile-lifecycle Swift harness (suspend/resume cycles) against +# a goose iroh ACP server. +# +# 1. cargo build -p goose-cli --bin goose -p goose-tunnel-ffi +# 2. ./target/debug/goose serve --iroh # copy the printed connection token +# 3. crates/goose-tunnel-ffi/swift-driver/run-mobile.sh "" +set -euo pipefail + +TOKEN="${1:?usage: run-mobile.sh }" +REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +LIBDIR="$REPO_ROOT/target/debug" +DRIVER_DIR="$(cd "$(dirname "$0")" && pwd)" +BUILD_DIR="$(mktemp -d)" + +cargo build -p goose-tunnel-ffi --manifest-path "$REPO_ROOT/Cargo.toml" >/dev/null + +cargo run -p goose-tunnel-ffi --bin uniffi-bindgen --manifest-path "$REPO_ROOT/Cargo.toml" -- \ + generate --library "$LIBDIR/libgoose_tunnel_ffi.dylib" --language swift --out-dir "$BUILD_DIR" >/dev/null 2>&1 + +echo 'module goose_tunnel_ffiFFI { header "goose_tunnel_ffiFFI.h" export * }' > "$BUILD_DIR/module.modulemap" + +swiftc -parse-as-library -o "$BUILD_DIR/mobile" \ + "$DRIVER_DIR/mobile-lifecycle.swift" "$BUILD_DIR/goose_tunnel_ffi.swift" \ + -I "$BUILD_DIR" \ + -L "$LIBDIR" -lgoose_tunnel_ffi \ + -Xcc -fmodule-map-file="$BUILD_DIR/module.modulemap" + +DYLD_LIBRARY_PATH="$LIBDIR" "$BUILD_DIR/mobile" "$TOKEN" diff --git a/crates/goose-tunnel-ffi/swift-driver/run.sh b/crates/goose-tunnel-ffi/swift-driver/run.sh new file mode 100755 index 0000000000..85cf64a8b2 --- /dev/null +++ b/crates/goose-tunnel-ffi/swift-driver/run.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Build + run the Swift end-to-end driver against a goose iroh ACP server. +# +# 1. cargo build -p goose-cli --bin goose -p goose-tunnel-ffi +# 2. ./target/debug/goose serve --iroh # copy the printed connection token +# 3. crates/goose-tunnel-ffi/swift-driver/run.sh "" +set -euo pipefail + +TOKEN="${1:?usage: run.sh }" +REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +LIBDIR="$REPO_ROOT/target/debug" +DRIVER_DIR="$(cd "$(dirname "$0")" && pwd)" +BUILD_DIR="$(mktemp -d)" + +cargo build -p goose-tunnel-ffi --manifest-path "$REPO_ROOT/Cargo.toml" >/dev/null + +cargo run -p goose-tunnel-ffi --bin uniffi-bindgen --manifest-path "$REPO_ROOT/Cargo.toml" -- \ + generate --library "$LIBDIR/libgoose_tunnel_ffi.dylib" --language swift --out-dir "$BUILD_DIR" >/dev/null 2>&1 + +echo 'module goose_tunnel_ffiFFI { header "goose_tunnel_ffiFFI.h" export * }' > "$BUILD_DIR/module.modulemap" + +swiftc -o "$BUILD_DIR/driver" "$DRIVER_DIR/main.swift" "$BUILD_DIR/goose_tunnel_ffi.swift" \ + -I "$BUILD_DIR" \ + -L "$LIBDIR" -lgoose_tunnel_ffi \ + -Xcc -fmodule-map-file="$BUILD_DIR/module.modulemap" + +DYLD_LIBRARY_PATH="$LIBDIR" "$BUILD_DIR/driver" "$TOKEN" diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index fccd7dbbc6..116ecf3a18 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -215,6 +215,7 @@ pctx_code_mode = { version = "0.3", default-features = false, optional = true } icu_calendar = { version = "=2.1.1", default-features = false } icu_locale = { version = "=2.1.1", default-features = false } llama-cpp-sys-2 = { workspace = true, optional = true } +iroh = "1.0.0-rc.1" [target.'cfg(target_os = "windows")'.dependencies] winapi = { workspace = true } diff --git a/crates/goose/src/acp/transport/iroh.rs b/crates/goose/src/acp/transport/iroh.rs new file mode 100644 index 0000000000..6d8d707bda --- /dev/null +++ b/crates/goose/src/acp/transport/iroh.rs @@ -0,0 +1,139 @@ +//! iroh/QUIC transport for ACP. +//! +//! Serves ACP as newline-delimited JSON-RPC directly on an iroh QUIC bidi stream +//! (no HTTP layer). Each accepted bidi stream gets its own agent via +//! `acp::server::serve`, exactly like the stdio and channel-backed transports β€” +//! only the byte pipe changes. +//! +//! iroh provides: peer discovery (relay + hole-punched direct path), NodeId +//! identity (ed25519 public key, mutually verified by QUIC TLS 1.3), and an +//! authenticated QUIC connection. We bake in default relays so a paired client +//! can find the server behind NAT. + +use std::sync::Arc; + +use anyhow::{Context, Result}; +use iroh::{ + endpoint::{Endpoint, RecvStream, SendStream}, + RelayMap, RelayMode, SecretKey, +}; +use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; +use tracing::{error, info, warn}; + +use crate::acp::server_factory::AcpServer; + +/// ALPN for goose ACP over QUIC. Bumped if the on-wire ACP framing changes. +pub const ALPN_GOOSE_ACP_V1: &[u8] = b"goose-acp/1"; + +/// Default relays the client and server share so a phone can find a laptop +/// behind NAT. Overridable via `GOOSE_IROH_RELAYS` (comma-separated URLs). +pub const DEFAULT_RELAYS: &[&str] = &[ + "https://usw1-2.relay.michaelneale.mesh-llm.iroh.link./", + "https://aps1-1.relay.michaelneale.mesh-llm.iroh.link./", +]; + +fn effective_relay_urls() -> Vec { + match std::env::var("GOOSE_IROH_RELAYS") { + Ok(v) if !v.trim().is_empty() => v + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(), + _ => DEFAULT_RELAYS.iter().map(|s| s.to_string()).collect(), + } +} + +fn relay_mode() -> Result { + let urls = effective_relay_urls(); + let configs = urls + .iter() + .map(|u| { + u.parse::() + .map(|url| iroh::RelayConfig::new(url, None)) + .with_context(|| format!("invalid relay URL: {u}")) + }) + .collect::>>()?; + Ok(RelayMode::Custom(RelayMap::from_iter(configs))) +} + +/// Bind an iroh endpoint for serving ACP, returning the endpoint and the +/// base64url-encoded `EndpointAddr` token a client uses to connect (the QR +/// payload). `secret_key` persists the server's NodeId across restarts. +pub async fn bind_server(secret_key: SecretKey) -> Result<(Endpoint, String)> { + let endpoint = Endpoint::builder(iroh::endpoint::presets::Minimal) + .secret_key(secret_key) + .alpns(vec![ALPN_GOOSE_ACP_V1.to_vec()]) + .relay_mode(relay_mode()?) + .bind() + .await?; + + // Wait until we're reachable via a relay so the advertised addr is usable. + let _ = tokio::time::timeout(std::time::Duration::from_secs(10), endpoint.online()).await; + + let addr = endpoint.addr(); + let token = encode_addr_token(&addr)?; + info!(node_id = %endpoint.id(), "iroh ACP endpoint bound"); + Ok((endpoint, token)) +} + +pub fn encode_addr_token(addr: &iroh::EndpointAddr) -> Result { + use base64::Engine as _; + let json = serde_json::to_vec(addr)?; + Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(json)) +} + +pub fn decode_addr_token(token: &str) -> Result { + use base64::Engine as _; + let json = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(token) + .context("invalid endpoint token encoding")?; + serde_json::from_slice(&json).context("invalid endpoint token JSON") +} + +/// Accept loop: each incoming connection may open multiple ACP bidi streams; +/// each stream is served by its own agent. +pub async fn serve(endpoint: Endpoint, server: Arc) -> Result<()> { + info!("iroh ACP transport accepting connections"); + while let Some(incoming) = endpoint.accept().await { + let server = server.clone(); + tokio::spawn(async move { + if let Err(e) = handle_connection(incoming, server).await { + warn!("iroh connection ended: {e}"); + } + }); + } + Ok(()) +} + +async fn handle_connection( + incoming: iroh::endpoint::Incoming, + server: Arc, +) -> Result<()> { + let connection = incoming.await?; + let remote = connection.remote_id(); + info!(peer = %remote, "iroh ACP connection established"); + + loop { + match connection.accept_bi().await { + Ok((send, recv)) => { + let server = server.clone(); + tokio::spawn(async move { + if let Err(e) = handle_stream(send, recv, server).await { + error!("iroh ACP stream error: {e}"); + } + }); + } + Err(e) => { + info!(peer = %remote, "iroh connection closed: {e}"); + break; + } + } + } + Ok(()) +} + +async fn handle_stream(send: SendStream, recv: RecvStream, server: Arc) -> Result<()> { + let agent = server.create_agent().await?; + // QUIC streams are tokio AsyncRead/AsyncWrite; serve() wants futures-io. + crate::acp::server::serve(agent, recv.compat(), send.compat_write()).await +} diff --git a/crates/goose/src/acp/transport/mod.rs b/crates/goose/src/acp/transport/mod.rs index ab0ca41da3..00e6cd755d 100644 --- a/crates/goose/src/acp/transport/mod.rs +++ b/crates/goose/src/acp/transport/mod.rs @@ -1,5 +1,6 @@ pub mod connection; pub mod http; +pub mod iroh; pub mod websocket; use std::sync::Arc; diff --git a/documentation/notes/mobile-tunnel-iroh-prototype.md b/documentation/notes/mobile-tunnel-iroh-prototype.md new file mode 100644 index 0000000000..688f300ca9 --- /dev/null +++ b/documentation/notes/mobile-tunnel-iroh-prototype.md @@ -0,0 +1,150 @@ +# ACP-over-iroh mobile tunnel β€” working prototype + +> Status: **end-to-end proven** on 2026-06-01. Pure-Swift client β†’ goose-owned +> FFI β†’ iroh QUIC (relay + direct) β†’ goosed ACP, both direct and relay paths. +> All changes are local prototype code; see "What's prototype-quality" below. + +## What was built + +### 1. Server: iroh ACP transport in goose +`crates/goose/src/acp/transport/iroh.rs` (new) β€” binds an iroh `Endpoint` +(ALPN `goose-acp/1`), registers with the two baked-in relays +(`*.relay.michaelneale.mesh-llm.iroh.link`, override via `GOOSE_IROH_RELAYS`), +accepts QUIC connections, and serves **ACP as newline-delimited JSON-RPC directly +on each bidi stream** via the existing `acp::server::serve` (Option A β€” no HTTP +layer). Also encodes/decodes the `EndpointAddr` connection token (the QR payload). + +Launch: `crates/goose-cli/src/cli.rs` β€” `goose serve --iroh`. Persists a stable +NodeId in `~/.config/goose/iroh_acp_secret.key`, prints the NodeId + base64url +connection token. + +### 2. Client FFI: goose-owned, not iroh-ffi +`crates/goose-tunnel-ffi/` (new crate) β€” a small UniFFI surface, **not** n0's broad +`iroh-ffi`/`IrohLib`. Exposes only: +- `generate_device_keypair() -> String` (hex; NodeId = the device identity) +- `connect(server_token, device_key_hex, listener) -> GooseTunnel` +- `GooseTunnel.send(line)` / `.path_kind() -> Direct|Relayed|Connecting` / `.disconnect()` +- `trait MessageListener { on_message(line); on_closed(reason) }` + +Internally: iroh client `Endpoint`, relays baked in, opens one ACP bidi stream, +newline-frames JSON-RPC both directions. **ACP logic stays in the (Swift) caller** β€” +the FFI is just the authenticated byte pipe. + +### 3. Swift driver (end-to-end proof, in-repo) +`crates/goose-tunnel-ffi/swift-driver/{main.swift,run.sh}` β€” pure Swift, links the +generated UniFFI bindings + the dylib. Generates a device key, connects via a token, +sends ACP `initialize`, parses the response. `run.sh` regenerates bindings, compiles, +and runs in one step. (A proof harness; the real iOS app would reuse its existing ACP +client over the same FFI.) + +## What was verified (live) + +``` +# direct path (same LAN β€” token has Relay + Ip): +πŸ›£οΈ path: direct +➑️ {"jsonrpc":"2.0","id":1,"method":"initialize",...} +⬅️ {"jsonrpc":"2.0","result":{"protocolVersion":1,"agentCapabilities":{...}}} +βœ… ACP initialize round-trip succeeded over iroh (exit 0) + +# relay path (off-network sim β€” token stripped to Relay only): +πŸ›£οΈ path: relayed +⬅️ {"jsonrpc":"2.0","result":{...}} (exit 0) +``` + +- **Direct path:** iroh hole-punched a direct QUIC path on the LAN automatically. +- **Relay path:** with a relay-only token (the real off-network mobile case), + traffic flowed through the relay β€” E2E encrypted, relay sees only ciphertext. +- **Same ACP, both paths:** identical `initialize` round-trip; real goose agent + capabilities returned. +- **Identity:** QUIC TLS 1.3 mutually authenticates NodeIds; the device key is the + client's identity (pin it on pairing for authorization β€” not yet enforced, below). + +## How to reproduce +```bash +# 1. build +cargo build -p goose-cli --bin goose -p goose-tunnel-ffi + +# 2. server β€” prints NodeId + connection token +./target/debug/goose serve --iroh + +# 3. run the Swift driver end to end (regenerates bindings, compiles, runs) +crates/goose-tunnel-ffi/swift-driver/run.sh "" +``` +Verified reproducing from a clean tree on 2026-06-01: direct path (LAN) and relay +path (relay-only token) both exit 0 with a real ACP `initialize` round-trip. + +## Mobile lifecycle testing (tested like a phone, not just a CLI) + +A plain end-to-end run is a desktop-style process. What makes it *mobile* is the +lifecycle: suspend kills the UDP socket, foreground must reconnect fast, and the +path may be relay or direct. These were exercised with two extra in-repo harnesses: + +- `swift-driver/mobile-lifecycle.swift` (run via `run-mobile.sh`) β€” **3 + foreground/background cycles** with a **stable device identity** (generated once, + reused on every reconnect, like a Keychain-persisted key). Each foreground: + connect β†’ ACP `initialize` β†’ clean `disconnect()` (simulating iOS background + teardown). **Result: PASS** β€” every cycle reconnects cold and re-establishes ACP + in ~1.3–2.7s. +- `swift-driver/path-probe.swift` β€” connect with a **relay-only token** (the true + off-network/cellular case: client is given only the relay, no LAN address) and + sample `path_kind()` over time. + +### What this proved (honestly) +- **Suspend/resume works:** cold reconnect + ACP re-init succeeds repeatedly with a + stable NodeId. This is the core mobile loop. +- **Relay path is real:** with a relay-only token the path stays `relayed` for the + whole session and ACP completes over the relay (E2E encrypted, relay sees only + ciphertext). +- **Direct path is real:** with the full token (relay + LAN IP) iroh hole-punches to + `direct` automatically. + +### Honest single-host caveat +Client and server run on **one machine**, so a loopback direct path always exists. +After a relay-bootstrapped connection, iroh's disco can learn that loopback +candidate and upgrade `relayed β†’ direct` on later reconnects β€” correct iroh +behavior, but it means a *sustained* relay-only path can't be guaranteed on one +host. A truly permanent relay path (no direct possible) needs two machines / real +NAT / cellular, or network-level blocking of the direct path. The relay path itself +is proven (probe + cycle #1); its permanence under real NAT is not single-host +testable. + +### Not yet tested (needs a real device / Xcode app) +- iOS `scenePhase`/`NWPathMonitor`-driven reconnect (here it's simulated by explicit + disconnect/reconnect). +- QUIC connection migration on a live Wi-Fi↔cellular switch. +- Behavior across a real OS process freeze (vs. simulated teardown). +- Streaming `session/prompt` interrupted mid-turn by suspend + resume cursor. + +## What's prototype-quality (not production) + +1. **No device authorization yet.** Any client that has the token can connect + (transport identity is verified, but we don't pin/allow-list the device NodeId + on goosed). Next: capture device NodeId at QR-pairing, enforce an allow-list. +2. **One stream per connection, no resume cursor.** Suspend/resume (Β§4 of the + research note) β€” event-id cursor + `resume(session, after_event=N)` β€” not built. +3. **Token strips to relay-only manually** in the test; real client just uses the + full token and lets iroh choose (verified it does). +4. **FFI runs its own tokio runtime per connect** β€” fine for one tunnel; revisit + if multiple. +5. **Server reuses `acp::server::serve` per stream**, bypassing the + `ConnectionRegistry`/replay buffer. To get replay-on-reconnect, route through the + registry instead (small change; the registry is already transport-agnostic). +6. **iOS lifecycle glue not built** (no Xcode app here) β€” `scenePhase` + + `NWPathMonitor` β†’ reconnect, `beginBackgroundTask` clean-close. Design in Β§5 of + the research note. +7. **xcframework packaging not done** β€” bindings generated for macOS dylib only; + the iOS build pipeline (lipo + create-xcframework for arm64/sim) is the + `make_swift.sh`-style step from Β§8. +8. **iroh `1.0.0-rc.1`** pinned in both goose and the FFI crate β€” keep them in lock + step (shared relay/disco wire protocol). + +## Files changed +- `crates/goose/src/acp/transport/iroh.rs` (new) +- `crates/goose/src/acp/transport/mod.rs` (+`pub mod iroh;`) +- `crates/goose/Cargo.toml` (+`iroh`) +- `crates/goose-cli/src/cli.rs` (`--iroh` flag, `handle_serve_iroh_command`, key persistence) +- `crates/goose-cli/Cargo.toml` (+`iroh`, `hex`) +- `crates/goose-tunnel-ffi/` (new crate: `Cargo.toml`, `src/lib.rs`, `src/uniffi-bindgen.rs`) +- `crates/goose-tunnel-ffi/swift-driver/` (in-repo Swift harnesses + run scripts: + `main.swift`/`run.sh` = basic e2e; `mobile-lifecycle.swift`/`run-mobile.sh` = + suspend/resume cycles; `path-probe.swift` = relay-vs-direct path observation) diff --git a/documentation/notes/mobile-tunnel-iroh-research.md b/documentation/notes/mobile-tunnel-iroh-research.md new file mode 100644 index 0000000000..3c46c07b77 --- /dev/null +++ b/documentation/notes/mobile-tunnel-iroh-research.md @@ -0,0 +1,410 @@ +# Replacing the goose Mobile Tunnel with ACP over iroh/QUIC + +> Research note β€” exploratory, no implementation. Proposes throwing out the +> "lapstone" HTTP-over-WebSocket tunnel and replacing it with **ACP carried over +> iroh QUIC streams**, reachable from a **pure-Swift iOS app via a goose-owned FFI +> framework** (`Goose.xcframework`, built with the n0 `iroh-ffi` *recipe* but not +> shipping their broad binding β€” no Rust source in the app). Framing decision: ACP +> as newline-delimited JSON-RPC directly on the QUIC stream (Option A, Β§2). + +## 0. Corrections to earlier assumptions (read first) + +Two things I previously got wrong, now verified: + +1. **iroh *does* have an official Swift binding.** `github.com/n0-computer/iroh-ffi` + ships: + - a **prebuilt `Iroh.xcframework`** (`ios-arm64`, `ios-arm64_x86_64-simulator`, + `macos-arm64`), + - an **`IrohLib` Swift Package** + CocoaPods podspecs (`IrohLib.podspec`, + `IrohLibFramework.podspec`), + - a documented Swift usage guide (`README.swift.md`) and a + "build-your-own-binding" route (docs.iroh.computer/deployment/other-languages). + The binding is UniFFI-generated. So Swift consumes iroh as an **opaque binary + framework** β€” no Rust toolchain or Rust source in the app project. + +2. **You don't have to reimplement the relay/QUIC protocol, and you don't have to + author the FFI from scratch.** `iroh-ffi`/`IrohLib` is a reference/example with a + far-too-broad surface (blobs/docs/gossip) β€” we **don't ship it** (Β§8). Instead we + reuse its *build recipe* to produce a small **goose-owned** `Goose.xcframework` + exposing just connect + ACP stream + cancel, with our relays baked in. + +The plain-QUIC-gateway idea (previous "Option A") is **dropped**: a gateway that +terminates the phone's QUIC is just lapstone with a nicer transport β€” no direct +path, no end-to-end encryption. Not worth doing. + +## 1. What exists today (to be replaced) + +- **lapstone tunnel** (`crates/goose-server/src/tunnel/{mod.rs,lapstone.rs}`, + `routes/tunnel.rs`): goosed dials an outbound WebSocket to a **personal + Cloudflare Worker**; the iOS app hits that Worker over HTTPS. Every phone HTTP + request is reframed as JSON `TunnelMessage`/`TunnelResponse` over WS, with manual + chunking and SSE-streaming handling. +- **The mobile client talks HTTP+SSE, not ACP.** It hits goosed's REST surface β€” + notably `POST /reply` which returns `text/event-stream` + (`routes/reply.rs`: `SseResponse`, `Content-Type: text/event-stream`). The tunnel + is a generic HTTP proxy; the phone speaks the same HTTP API the desktop does. +- **ACP already has network transports.** Beyond stdio, goose has a + transport-agnostic ACP server at **`crates/goose/src/acp/transport/`**: + `http.rs`, `websocket.rs`, `connection.rs`, `mod.rs`. It serves ACP over an axum + router on `/acp` β€” `POST` (JSON-RPC request), `GET` (WebSocket upgrade *or* SSE), + `DELETE` (teardown), scoped by `Acp-Connection-Id` / `Acp-Session-Id` headers. + **This is the integration point β€” not stdio.** + +### The transport abstraction (this is what we hook into) +`connection.rs` defines a transport-agnostic `Connection` + `ConnectionRegistry`: +- `to_agent_tx: mpsc::Sender` β€” clientβ†’agent JSON-RPC lines. +- An outbound fan-out (`OutboundStream`, broadcast) for agentβ†’client, **with a + pre-subscribe replay buffer** (`subscribe_with_replay`): messages emitted before + a subscriber attaches are buffered and replayed on (re)subscribe. +- `adapters.rs` bridges mpsc ↔ `AsyncRead/AsyncWrite` with newline JSON-RPC framing. + +A concrete transport (`websocket::run_ws`) is **~70 lines**: split the socket, +replay buffered messages, then `select!` { socketβ†’`to_agent_tx` ; `outbound_rx`β†’ +socket }. **An iroh transport is the same ~70 lines over a QUIC bidi stream** β€” the +`Connection`, registry, session routing, and replay buffer are all reused unchanged. +The replay buffer is also **most of the suspend/resume story already** (reconnect β†’ +replay); we only extend the cursor to survive a full reconnect (Β§4). + +### Why replace it +- Single personal relay = SPOF + trust anchor; **Cloudflare terminates TLS and + sees plaintext** (no E2E). +- **Never a direct path** β€” even same-Wi-Fi traffic round-trips through the Worker. +- Auth is a **shared 32-byte bearer secret** in the QR (plus a buggy + `secure_compare` using non-constant-time 64-bit `DefaultHasher` β€” fix regardless). +- Bespoke HTTP-over-JSON-over-WS reframing split across 3 repos; binary is lossy. + +## 2. Target architecture + +``` +iOS app (pure Swift) Goose.xcframework (goose-owned, vendored) laptop +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” goose- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” iroh QUIC β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ SwiftUI + ACP β”‚ tunnel β”‚ iroh Endpoint (client) β”‚ ──relay──► β”‚ goosed β”‚ +β”‚ client (Swift) β”‚ Swift API β”‚ ALPN goose-acp/1 β”‚ ◄─direct──► β”‚ +iroh β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ─────────► β”‚ 2 relays baked in β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + no Rust source β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ E2E encrypted; relay + sees ciphertext only +``` + +- **goosed** binds an iroh `Endpoint` (ALPN `goose-acp/1`), registers with the two + relays via `RelayMode::Custom`, runs an accept loop, and serves **ACP as + newline-delimited JSON-RPC on each accepted QUIC bidi stream** (Option A below). +- **iOS app** uses the goose-owned `Goose.xcframework` (Β§8) to dial the server's + `NodeAddr`, open a bidi QUIC stream, and speak the **same newline JSON-RPC ACP it + already uses** β€” only the byte pipe changes. The two relays are compiled-in defaults. +- On LAN: connects via relay, then **hole-punches to a direct path** automatically. + Off-net: rides the relay, **end-to-end encrypted** (relay can't read traffic). + +**Server change is small because ACP transports are already pluggable.** goosed adds +a `crates/goose/src/acp/transport/iroh.rs` next to `websocket.rs`: bind an iroh +`Endpoint` (ALPN `goose-acp/1`, 2 relays via `RelayMode::Custom`), accept loop β†’ +for each QUIC bidi stream call `registry.create_connection()` and run the same +`select!` bridge as `run_ws`. No new ACP semantics, no protocol redesign β€” the +agent loop, session routing, and replay buffer are reused verbatim. + +### The layer stack β€” and the one real decision + +iroh is **not** a peer of HTTP. These are stacked layers; the only open choice is +the *framing* layer: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ACP JSON-RPC 2.0 (initialize, session/prompt, …) β”‚ the protocol +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ FRAMING newline-delimited JSON-RPC ← THE choice (Option A) β”‚ ← decided: A +β”‚ (alt: HTTP/3 over the top β€” rejected, Option B) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ STREAM a reliable, ordered, bidi byte stream β”‚ +β”‚ == iroh QUIC bidi stream (SendStream/RecvStream) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ QUIC streams + TLS 1.3 + multiplexing + migration β”‚ (inside iroh) +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ iroh discover peer + NodeId identity + relay/holepunch β”‚ finds + authenticates +β”‚ β†’ hands you an authenticated QUIC connection β”‚ β†’ a QUIC connection +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**iroh's job:** find the goose server (relay or direct), authenticate it by NodeId, +and produce an authenticated QUIC connection. **ACP's job:** ride a byte stream as +JSON-RPC lines. A QUIC bidi stream *is* the byte stream β€” they meet directly. + +### DECISION: Option A β€” ACP as newline-delimited JSON-RPC directly on the QUIC stream + +**No HTTP layer.** This is not a close call: + +1. **Zero impedance with the agent core.** `acp::server::serve(agent, read, write)` + already consumes a generic `AsyncRead`/`AsyncWrite` of newline-delimited JSON-RPC + β€” exactly what stdio uses. A QUIC `SendStream`/`RecvStream` *is* that pair, so the + QUIC stream feeds `serve()` directly (via the existing `ReceiverToAsyncRead`/ + `compat` adapters). Option B would stand up an HTTP server on the QUIC connection + only to re-derive the same byte stream the agent then re-parses. +2. **HTTP framing solves problems we no longer have.** The `/acp` POST/GET-as-SSE/ + DELETE routes + `Acp-*` headers + chunking + content-type sniffing exist to + smuggle a long-lived **bidirectional** JSON-RPC conversation through request/ + response HTTP. A QUIC bidi stream is natively bidirectional and long-lived β€” + HTTP-over-the-top re-introduces the very SSE/chunking machinery we're deleting. +3. **Smallest, lowest-risk change.** One file, `transport/iroh.rs`, mirroring + `websocket.rs`'s ~70-line `select!` loop over QUIC streams. `Connection`, + `ConnectionRegistry`, session routing, and the replay buffer reused verbatim. + No HTTP/3 server, no route re-mounting, no SSE plumbing. +4. **Proven shape.** mesh-llm runs its protocol as frames straight on iroh QUIC + streams (no HTTP) β€” same shape as ACP-on-QUIC. +5. **Cleaner suspend/resume.** Owning the framing end-to-end lets us add a resume + cursor (event-id on notifications, "resume after N" on reconnect) as a protocol + decision we control, instead of leaning on HTTP/SSE `Last-Event-ID` semantics and + gateway-style buffering (Β§4). + +**Framing sub-choice:** use **newline-delimited JSON-RPC** (identical to stdio + WS +today β†’ maximum reuse). Length-prefixed frames (mesh-llm style) are marginally more +robust for large/binary payloads but unnecessary for line-oriented JSON ACP β€” don't +add unless a concrete need appears. + +**Option B (HTTP/3 over QUIC) β€” rejected.** Only justifiable to reuse the exact +existing `/acp` HTTP handlers unchanged for a throwaway prototype; the transport +abstraction is already thin enough that the saving is tiny and the HTTP layer is +permanent overhead. Not worth it. + +Net: **one protocol, one transport.** Mobile speaks ACP over an iroh QUIC stream; +desktop/CLI keep their existing transports. The HTTP REST surface (`/reply`, the +`/acp` HTTP routes) stays for the desktop, but mobile no longer depends on it. + +## 3. Identity & end-to-end auth (replaces the shared secret) + +- **Transport identity is free from iroh/QUIC.** Each endpoint is an ed25519 + keypair; **NodeId == public key**, mutually verified by QUIC's TLS 1.3 handshake. + goosed learns the phone's verified public key; the phone learns goosed's. No + bearer secret needed to prove *who* you're talking to. +- **Authorization = per-device NodeId pinning.** At QR-pairing time, capture the + phone's NodeId into an allow-list in goose config. Only enrolled devices connect; + revoke one device without rotating a global secret. (mesh-llm's + `SignedNodeOwnership` certs over NodeIds are a ready blueprint if we want signed + enrollment rather than a plain allow-list.) +- **Pairing QR** carries `base64url(NodeAddr)` = server NodeId + relay URLs + (iroh's `EndpointAddr`/`NodeAddr` token, cf. mesh-llm + `encode/decode_endpoint_addr_token`), replacing `goosechat://configure?{url,secret}`. +- Local `server_secret` can still gate the loopback bridge as defense-in-depth; it + never leaves the machine. + +## 4. Operational behavior on iOS β€” the suspend/resume lifecycle + +**Fundamental constraint (transport-independent):** iroh runs over UDP on a tokio +runtime *inside* the xcframework. When iOS suspends the app it **freezes the +process** β€” tokio stops scheduling, keepalives stop, the relay session and any +direct path hit their idle timeout and are reaped server-side. **No entitlement +keeps a general data app's UDP socket alive** in the background (VoIP/PushKit is +for calls; Apple rejects misuse). Design rule: **don't survive suspension β€” +survive *resumption* fast.** + +| Phase | Behavior | Action | +|---|---|---| +| Foreground, LAN | relay first β†’ **hole-punch to direct QUIC** in ~1-2s | observe via `home_relay()`/`remote_info_list()` | +| Foreground, off-net | relay path, **ciphertext-only** through relay | normal operation | +| Backgrounding | ~seconds before freeze | **clean `disconnect()`** in `beginBackgroundTask`; persist stream cursor; mark `Suspended` | +| Frozen | nothing runs | fine β€” don't fight it | +| Foreground resume | re-`connect()`; QUIC **0-RTT session resume (~ms)**; auto direct-path re-upgrade | trigger on `scenePhase == .active` | +| Roaming (Wi-Fi↔cellular, foregrounded) | QUIC **connection migration**, often seamless (no reconnect) | trigger on `NWPathMonitor` change | +| Mid-stream across suspend | stream dies (any transport) | **app-level resume cursor** (see below) | +| Wake while asleep | impossible over iroh (frozen phone can't receive UDP) | needs **APNs push** to foreground first | + +**vs. lapstone:** background-drop is the same (unavoidable on iOS), but resume is +**0-RTT QUIC vs. full TCP+TLS+WS reconnect**, roaming can be **seamless migration +vs. full reconnect**, and foreground gives **direct path + true E2E** that lapstone +never has. + +### Streaming continuity across suspend (iroh does NOT solve this) +A QUIC stream carrying an in-flight ACP turn dies on freeze. Design ACP-over-iroh +with a **resume cursor**: +- goosed tags each `session/update` with a monotonic event id. +- On reconnect the client sends `resume(session_id, after_event=N)`. +- goosed replays buffered tail events, or returns the completed turn if it finished + while the app was away. +This is the same problem lapstone has today; it's an application-protocol concern. + +### "Notify me when it's done while asleep" +Only achievable with **APNs**: a relay-side or goosed-side hook fires a push β†’ app +foregrounds β†’ iroh reconnects (0-RTT) β†’ client pulls the result via resume cursor. +Orthogonal to transport; the only path to asleep-delivery on iOS. + +## 5. Minimal Swift lifecycle glue (sketch) + +```swift +// One "ensure connected" routine driven by both lifecycle and network triggers. +@MainActor final class GooseTunnel: ObservableObject { + @Published var path: PathKind = .disconnected // .direct / .relayed / .suspended + + private let node: GooseTunnelNode // from goose-owned Goose.xcframework (Β§8) + private var conn: GooseAcpStream? + private var resumeCursor: UInt64 = 0 + private let serverAddr: NodeAddr // decoded from paired QR (base64url) + + func ensureConnected() async { + guard conn == nil else { return } + conn = try? await node.connectAcp(serverAddr, + resumeAfter: resumeCursor) // 0-RTT when possible + path = (try? await node.isDirect(serverAddr)) == true ? .direct : .relayed + } + + func onForeground() { Task { await ensureConnected() } } // scenePhase == .active + + func onPathChange(_ p: NWPath) { // NWPathMonitor + // QUIC migration usually handles this transparently when foregrounded; + // only re-dial if the stream actually dropped. + if conn == nil { Task { await ensureConnected() } } + } + + func onBackground(_ task: UIBackgroundTaskID) { // beginBackgroundTask + conn?.closeCleanly() // don't leave a zombie + conn = nil + path = .suspended + // resumeCursor already persisted as updates arrive + } +} +``` +The `connectAcp` / `isDirect` / `closeCleanly` / event-id cursor methods are what +the **goose-owned FFI surface** (`crates/goose-tunnel-ffi`, Β§8) exposes. Internally +it uses iroh's `Endpoint` + `node_addr`/`home_relay`/`remote_info_list` to decide +direct-vs-relayed and to open the ACP bidi stream β€” but the Swift app only sees the +small goose-shaped API, not iroh's full surface. + +## 6. Implementation shape (when we act) + +**Server (Rust, in goose) β€” small, because ACP transports are pluggable:** +1. Add **`crates/goose/src/acp/transport/iroh.rs`** next to `websocket.rs`: bind iroh + `Endpoint` (ALPN `goose-acp/1`, `RelayMode::Custom` with the two baked-in relays), + accept loop β†’ per QUIC bidi stream `registry.create_connection()` + the same + `select!` bridge `run_ws` already uses. **Reuses `Connection`, registry, session + routing, and the replay buffer verbatim** β€” no new ACP semantics. +2. Wire it into goosed startup (where `tunnel/mod.rs` is today): bind endpoint, + carry over the watchdog/reconnect + single-instance lock. +3. Device enrollment: NodeId allow-list in goose config; QR emits `base64url(NodeAddr)`. +4. Extend the replay buffer with a **persistent per-session event cursor** so a full + reconnect (post-suspend) resumes instead of replaying-from-attach only (Β§4). +5. Delete lapstone (`tunnel/lapstone.rs`, Cloudflare Worker dependency, + `tunnel_secret` plumbing, `secure_compare`). + +**Client (Swift app) β€” talks the *same* ACP it already speaks, just over iroh:** +1. Vendor the goose-owned `Goose.xcframework` (Β§8) β€” exposes connect + ACP stream. +2. The existing Swift ACP/JSON-RPC client logic is reused; only the byte transport + changes (iroh bidi stream instead of the HTTP/WS-via-Worker it uses today). +3. Lifecycle glue (Β§5): `scenePhase` + `NWPathMonitor` β†’ `ensureConnected`. +4. QR pairing β†’ store `NodeAddr` + device keypair (Keychain). +5. (Later) APNs for asleep-delivery. + +**Relays:** +- Run our own iroh relays (cf. mesh-llm's `usw1-2 / aps1-1.relay.…iroh.link`), + fall back to n0 default relays, make configurable β€” mirror `effective_relay_urls`. + +## 7. Trade-offs summary + +| Concern | lapstone (today) | ACP over iroh | +|---|---|---| +| LAN datapath | always via Cloudflare | **direct (hole-punched)** | +| Off-net datapath | via Cloudflare (plaintext) | via relay (**ciphertext only**) | +| E2E encryption to app | no | **yes** | +| Identity/auth | shared 32-byte secret | **per-device ed25519 NodeId, mutually verified** | +| Relay trust | personal Worker, SPOF | self-hostable iroh relays, can't read traffic | +| Protocol | HTTPβ†’JSONβ†’WS reframe + manual chunk/stream | **native ACP over QUIC bidi stream** | +| Binary payloads | lossy (UTF-8 strings) | native bytes | +| Swift client | HTTP via Worker (works, no Rust) | **goose-owned `Goose.xcframework` β€” no Rust source in app** | +| Background drop | unavoidable | unavoidable (same) | +| Resume cost | full TCP+TLS+WS | **0-RTT QUIC** | +| Roaming | full reconnect | **QUIC migration (often seamless)** | +| Mid-stream resume | app-level cursor | app-level cursor (same) | +| Wake while asleep | needs push | needs push (same) | +| New infra | Cloudflare Worker | iroh relay server(s) (or n0 defaults) | + +## 8. Roll our own FFI β€” don't ship `iroh-ffi`/`IrohLib` + +`n0-computer/iroh-ffi` is explicitly a **reference/example**, not a product +dependency (`publish = false`; README says "for example"). Its surface is the +*whole* iroh toolkit β€” `src/{blob,doc,gossip,author,tag,node,net,endpoint,key, +ticket}.rs` (blobs, docs, gossip, authors…) β€” none of which we want on a mobile +ACP tunnel. We want a **tiny goose-owned crate** that exposes only: connect to a +paired goosed, open an ACP stream, push/pull ACP frames, resume cursor, cancel, +disconnect, observe path (direct/relayed). Pinning to their broad, fast-moving +binding (currently iroh `0.35`, UniFFI `0.28`) would drag in surface and version +churn we don't control. + +**What we copy from `iroh-ffi` is the *recipe*, not the code:** +- Crate layout: `crate-type = ["staticlib", "cdylib"]`, `uniffi::setup_scaffolding!()`, + a `uniffi-bindgen` bin, `lto = true`, deps = just `iroh` + `tokio` + our ACP types. +- Build pipeline (`make_swift.sh`): `cargo build --release` for + `aarch64-apple-ios`, `aarch64-apple-ios-sim`, `x86_64-apple-ios`, + `aarch64-apple-darwin` β†’ `uniffi-bindgen generate --language swift` β†’ + `lipo` the sim archs β†’ assemble `Goose.xcframework` (ios-arm64 + sim + macos) β†’ + generate the Swift interface β†’ `swift package compute-checksum` for a binary + Swift Package target. ~80 lines of shell we adapt verbatim. + +**Proposed `crates/goose-tunnel-ffi/`** (or live alongside the existing client SDK): +``` +goose-tunnel-ffi/ + Cargo.toml # iroh, tokio, uniffi, goose ACP types only + src/lib.rs # uniffi::setup_scaffolding!() + src/tunnel.rs # #[uniffi::export] GooseTunnel: connect/stream/cancel/status + uniffi-bindgen.rs + make_swift.sh # adapted from iroh-ffi + GooseTunnel.xcframework (generated artifact, or released as a zip) +``` +Exported surface (UniFFI), goose-shaped, ALPN `goose-acp/1`, relays baked in: +``` +generate_device_keypair() -> String +connect(server_addr_token, device_keypair, resume_after) -> GooseTunnel // async, 0-RTT capable +GooseTunnel.send_acp(json) +GooseTunnel.stream(req, listener: AcpEventListener) // callback iface, replaces SSE +GooseTunnel.cancel(request_id) +GooseTunnel.path_kind() -> Direct | Relayed // from remote_info_list/home_relay +GooseTunnel.disconnect() +``` +The Swift app imports this single framework β€” no Rust source, no toolchain, and a +surface we own and version with goose, not with n0's example repo. + +> Server side reuses the same `iroh` crate directly in goosed (no FFI there) β€” see Β§6. + +### Where does the Rust live? (three viable placements) + +The FFI crate is normal Rust; the only question is *which repo/build owns it*. The +Swift app always consumes a **binary `.xcframework`** regardless β€” so "Rust in the +Swift codebase" is really about whether the Rust *source* and its build sit in the +mobile repo, the goose monorepo, or standalone. + +1. **In the goose monorepo** (`crates/goose-tunnel-ffi/`) β€” *recommended default.* + - Pros: one `iroh` version pinned across goosed + FFI (critical β€” Β§9.6); shares + ACP types with `crates/goose/src/acp`; CI builds the xcframework as a release + artifact; client/server protocol can't drift. + - Cons: mobile release cadence coupled to monorepo; iOS toolchain in goose CI. + - The Swift app references the published `Goose.xcframework` (versioned release + zip + checksum), exactly how mesh-llm's `Package.swift` pulls a remote + `MeshLLMFFI.xcframework.zip`. + +2. **In the Swift app repo** (Rust crate as a subdirectory, built by an Xcode + build phase / `make_swift.sh`). + - Pros: mobile team owns cadence; Rust+Swift co-located, one PR changes both. + - Cons: must vendor/track the right `iroh` + goose ACP types out-of-tree; easy + for server and client iroh versions to drift (the Β§9.6 risk); duplicates the + Rust toolchain into the mobile repo. This is the literal "Rust in the Swift + codebase" option β€” it *works* (mesh-llm proves the build), but the version-skew + risk makes it weaker than (1) for a protocol shared with goosed. + +3. **Its own repo/crate** (`goose-tunnel` published crate + released xcframework). + - Pros: clean dependency for both goosed and the app; independent versioning; + reusable by a future Android/Kotlin client (UniFFI already does Kotlin). + - Cons: a third repo to release-coordinate; still needs the iroh-version pin + enforced across three consumers. + +**Recommendation:** start with (1) β€” crate in the monorepo, xcframework as a CI +release artifact the Swift app consumes by version. It eliminates the protocol/iroh +drift risk by construction. Promote to (3) only if/when a second client (Android) +or external consumers appear. (2) is fine for a fast prototype but I'd avoid it as +the long-term home because the client and server share the iroh wire protocol and +must move together. + +## 9. Open questions / next steps + +1. **ACP as a first-class network transport** β€” generalize the stdio ACP server + over a stream, or bridge-to-HTTP first for a faster cutover? +2. Own relays vs. n0 default vs. configurable (likely all three). +3. Enrollment UX: plain NodeId allow-list vs. signed ownership certs (mesh-llm style). +4. Migration: ship iroh transport behind a flag alongside lapstone, then delete. +5. APNs design for asleep-delivery (relay-side vs. goosed-side hook). +6. Pin a single `iroh` version across goosed + `goose-tunnel-ffi` (server and FFI + must speak the same relay/disco protocol β€” it changes between iroh versions). +7. Replace `secure_compare` now regardless (security fix to current code).