diff --git a/ui/goose2/src-tauri/Cargo.lock b/ui/goose2/src-tauri/Cargo.lock index d5da96d2af..cc49b2be2c 100644 --- a/ui/goose2/src-tauri/Cargo.lock +++ b/ui/goose2/src-tauri/Cargo.lock @@ -25,6 +25,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "agent-client-protocol" version = "0.10.4" @@ -358,6 +369,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -565,6 +585,15 @@ dependencies = [ "toml 0.9.12+spec-1.1.0", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.58" @@ -628,6 +657,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "combine" version = "4.6.7" @@ -672,6 +711,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -695,7 +744,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", "foreign-types", "libc", @@ -708,7 +757,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -845,6 +894,35 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +[[package]] +name = "dbus" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "dbus-secret-service" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" +dependencies = [ + "aes", + "block-padding", + "cbc", + "dbus", + "fastrand", + "hkdf", + "num", + "once_cell", + "sha2", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -899,6 +977,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -1112,6 +1191,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "event-listener" version = "5.4.1" @@ -1625,7 +1715,9 @@ dependencies = [ "chrono", "dirs", "doctor", + "etcetera", "futures", + "keyring", "log", "serde", "serde_json", @@ -1744,6 +1836,33 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "html5ever" version = "0.29.1" @@ -2034,6 +2153,16 @@ dependencies = [ "cfb", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -2187,6 +2316,23 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "byteorder", + "dbus-secret-service", + "linux-keyutils", + "log", + "secret-service", + "security-framework 2.11.1", + "security-framework 3.7.0", + "windows-sys 0.60.2", + "zeroize", +] + [[package]] name = "kuchikiki" version = "0.8.8-speedreader" @@ -2235,6 +2381,15 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + [[package]] name = "libloading" version = "0.7.4" @@ -2254,6 +2409,16 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-keyutils" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590" +dependencies = [ + "bitflags 2.11.0", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2441,6 +2606,7 @@ dependencies = [ "cfg-if", "cfg_aliases", "libc", + "memoffset", ] [[package]] @@ -2449,12 +2615,76 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3526,6 +3756,61 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "secret-service" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4d35ad99a181be0a60ffcbe85d680d98f87bdc4d7644ade319b87076b9dbfd4" +dependencies = [ + "aes", + "cbc", + "futures-util", + "generic-array", + "hkdf", + "num", + "once_cell", + "rand 0.8.5", + "serde", + "sha2", + "zbus 4.4.0", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.24.0" @@ -3891,6 +4176,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "string_cache" version = "0.8.9" @@ -3967,6 +4258,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -4041,7 +4338,7 @@ checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20" dependencies = [ "bitflags 2.11.0", "block2", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", "dispatch2", @@ -4307,7 +4604,7 @@ dependencies = [ "thiserror 2.0.18", "url", "windows", - "zbus", + "zbus 5.14.0", ] [[package]] @@ -5488,6 +5785,24 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -5530,6 +5845,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -5587,6 +5917,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5605,6 +5941,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -5623,6 +5965,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5653,6 +6001,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -5671,6 +6025,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -5689,6 +6049,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -5707,6 +6073,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -5924,6 +6296,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "yoke" version = "0.8.1" @@ -5947,6 +6329,38 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-process", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "ordered-stream", + "rand 0.8.5", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros 4.4.0", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + [[package]] name = "zbus" version = "5.14.0" @@ -5977,9 +6391,22 @@ dependencies = [ "uuid", "windows-sys 0.61.2", "winnow 0.7.15", - "zbus_macros", - "zbus_names", - "zvariant", + "zbus_macros 5.14.0", + "zbus_names 4.3.1", + "zvariant 5.10.0", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils 2.1.0", ] [[package]] @@ -5992,9 +6419,20 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.117", - "zbus_names", - "zvariant", - "zvariant_utils", + "zbus_names 4.3.1", + "zvariant 5.10.0", + "zvariant_utils 3.3.0", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant 4.2.0", ] [[package]] @@ -6005,7 +6443,7 @@ checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", "winnow 0.7.15", - "zvariant", + "zvariant 5.10.0", ] [[package]] @@ -6049,6 +6487,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "zerotrie" version = "0.2.3" @@ -6088,6 +6546,19 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive 4.2.0", +] + [[package]] name = "zvariant" version = "5.10.0" @@ -6098,8 +6569,21 @@ dependencies = [ "enumflags2", "serde", "winnow 0.7.15", - "zvariant_derive", - "zvariant_utils", + "zvariant_derive 5.10.0", + "zvariant_utils 3.3.0", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils 2.1.0", ] [[package]] @@ -6112,7 +6596,18 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.117", - "zvariant_utils", + "zvariant_utils 3.3.0", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] diff --git a/ui/goose2/src-tauri/Cargo.toml b/ui/goose2/src-tauri/Cargo.toml index a3149ff675..168704a83e 100644 --- a/ui/goose2/src-tauri/Cargo.toml +++ b/ui/goose2/src-tauri/Cargo.toml @@ -30,6 +30,7 @@ tokio = { version = "1.50.0", features = ["full"] } uuid = { version = "1", features = ["v4", "serde"] } chrono = { version = "0.4", features = ["serde"] } serde_yaml = "0.9" +etcetera = "0.8" futures = "0.3" tokio-stream = "0.1" tokio-util = { version = "0.7", features = ["compat", "rt"] } @@ -39,5 +40,14 @@ tokio-tungstenite = "0.21.0" acp-client = { git = "https://github.com/block/builderbot", rev = "6cb674a704f3a9e402a76e9aeed734144d6cec11" } doctor = { git = "https://github.com/block/builderbot", rev = "bfce29264181236a2e5b4fd903ace6291f6a7594" } +[target.'cfg(target_os = "macos")'.dependencies] +keyring = { version = "3", features = ["apple-native"] } + +[target.'cfg(target_os = "windows")'.dependencies] +keyring = { version = "3", features = ["windows-native"] } + +[target.'cfg(target_os = "linux")'.dependencies] +keyring = { version = "3", features = ["linux-native-sync-persistent", "crypto-rust"] } + [dev-dependencies] tempfile = "3" diff --git a/ui/goose2/src-tauri/src/commands/agent_setup.rs b/ui/goose2/src-tauri/src/commands/agent_setup.rs new file mode 100644 index 0000000000..7867e9cce2 --- /dev/null +++ b/ui/goose2/src-tauri/src/commands/agent_setup.rs @@ -0,0 +1,309 @@ +use std::path::PathBuf; +use tauri::{AppHandle, Emitter}; +use tokio::io::{AsyncBufReadExt, BufReader}; + +struct AgentCommandDef { + id: &'static str, + binary_name: &'static str, + install_command: Option<&'static str>, + auth_command: Option<&'static str>, + auth_status_command: Option<&'static str>, +} + +const AGENT_COMMAND_DEFS: &[AgentCommandDef] = &[ + AgentCommandDef { + id: "claude-acp", + binary_name: "claude-agent-acp", + install_command: Some( + "npm install -g @anthropic-ai/claude-code @zed-industries/claude-agent-acp", + ), + auth_command: Some("claude auth login"), + auth_status_command: Some("claude auth status"), + }, + AgentCommandDef { + id: "codex-acp", + binary_name: "codex-acp", + install_command: Some("npm install -g @openai/codex @zed-industries/codex-acp"), + auth_command: Some("codex login"), + auth_status_command: Some("codex login status"), + }, + AgentCommandDef { + id: "copilot-acp", + binary_name: "copilot", + install_command: Some("npm install -g @github/copilot"), + auth_command: Some("copilot login"), + auth_status_command: None, + }, + AgentCommandDef { + id: "amp-acp", + binary_name: "amp-acp", + install_command: Some("npm install -g @sourcegraph/amp@latest amp-acp"), + auth_command: Some("amp login"), + auth_status_command: Some("amp usage"), + }, + AgentCommandDef { + id: "cursor-agent", + binary_name: "cursor-agent", + install_command: Some("curl -fsSL https://cursor.com/install | bash"), + auth_command: Some("cursor-agent login"), + auth_status_command: Some("cursor-agent status"), + }, + AgentCommandDef { + id: "pi-acp", + binary_name: "pi-acp", + install_command: None, + auth_command: None, + auth_status_command: None, + }, +]; + +#[derive(serde::Serialize, Clone)] +#[serde(rename_all = "camelCase")] +struct AgentSetupOutput { + provider_id: String, + line: String, +} + +fn find_agent_command_def(provider_id: &str) -> Option<&'static AgentCommandDef> { + AGENT_COMMAND_DEFS.iter().find(|def| def.id == provider_id) +} + +fn get_agent_command_def(provider_id: &str) -> Result<&'static AgentCommandDef, String> { + find_agent_command_def(provider_id) + .ok_or_else(|| format!("Unknown agent provider '{provider_id}'")) +} + +fn build_extended_path() -> String { + let mut paths: Vec = Vec::new(); + + if let Ok(system_path) = std::env::var("PATH") { + paths.extend(std::env::split_paths(&system_path).filter(|p| { + !p.to_string_lossy().contains(".hermit") && !p.join("activate-hermit").exists() + })); + } + + if let Some(home) = dirs::home_dir() { + paths.push(home.join(".local/bin")); + paths.push(home.join(".npm-global/bin")); + } + + paths.push(PathBuf::from("/usr/local/bin")); + + #[cfg(target_os = "macos")] + { + paths.push(PathBuf::from("/opt/homebrew/bin")); + paths.push(PathBuf::from("/opt/local/bin")); + } + + if cfg!(windows) { + if let Some(appdata) = dirs::data_dir() { + paths.push(appdata.join("npm")); + } + } + + if let Some(home) = dirs::home_dir() { + let nvm_dir = home.join(".nvm/versions/node"); + if nvm_dir.exists() { + if let Ok(entries) = std::fs::read_dir(&nvm_dir) { + let mut versions: Vec<_> = entries + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false)) + .collect(); + versions.sort_by_key(|b| std::cmp::Reverse(b.file_name())); + if let Some(latest) = versions.first() { + paths.push(latest.path().join("bin")); + } + } + } + + let fnm_dir = home.join(".local/share/fnm/node-versions"); + if fnm_dir.exists() { + if let Ok(entries) = std::fs::read_dir(&fnm_dir) { + let mut versions: Vec<_> = entries + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false)) + .collect(); + versions.sort_by_key(|b| std::cmp::Reverse(b.file_name())); + if let Some(latest) = versions.first() { + paths.push(latest.path().join("installation/bin")); + } + } + } + } + + let mut seen = std::collections::HashSet::new(); + paths.retain(|p| seen.insert(p.clone())); + + std::env::join_paths(paths) + .unwrap_or_default() + .to_string_lossy() + .to_string() +} + +#[tauri::command] +pub async fn check_agent_installed(provider_id: String) -> Result { + let def = get_agent_command_def(&provider_id)?; + let extended_path = build_extended_path(); + + let (cmd, flag) = if cfg!(target_os = "windows") { + ("where", "/Q") + } else { + ("which", "") + }; + + let mut command = std::process::Command::new(cmd); + if !flag.is_empty() { + command.arg(flag); + } + command.arg(def.binary_name); + command.env("PATH", &extended_path); + + Ok(command + .output() + .map(|output| output.status.success()) + .unwrap_or(false)) +} + +#[tauri::command] +pub async fn check_agent_auth(provider_id: String) -> Result { + let def = get_agent_command_def(&provider_id)?; + let Some(auth_status_command) = def.auth_status_command else { + return Ok(false); + }; + + let extended_path = build_extended_path(); + + let shell = if cfg!(target_os = "windows") { + "cmd" + } else { + "sh" + }; + let flag = if cfg!(target_os = "windows") { + "/C" + } else { + "-c" + }; + + std::process::Command::new(shell) + .arg(flag) + .arg(auth_status_command) + .env("PATH", &extended_path) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .output() + .map(|output| output.status.success()) + .map_err(|e| format!("Failed to check auth status: {e}")) +} + +#[tauri::command] +pub async fn install_agent(app_handle: AppHandle, provider_id: String) -> Result<(), String> { + let def = get_agent_command_def(&provider_id)?; + let install_command = def + .install_command + .ok_or_else(|| format!("Agent provider '{provider_id}' does not support install"))?; + run_shell_command(&app_handle, &provider_id, install_command).await +} + +#[tauri::command] +pub async fn authenticate_agent(app_handle: AppHandle, provider_id: String) -> Result<(), String> { + let def = get_agent_command_def(&provider_id)?; + let auth_command = def + .auth_command + .ok_or_else(|| format!("Agent provider '{provider_id}' does not support auth"))?; + run_shell_command(&app_handle, &provider_id, auth_command).await +} + +fn strip_npm_config_env(cmd: &mut tokio::process::Command) { + for (key, _) in std::env::vars() { + if key.starts_with("npm_config") || key.starts_with("NPM_CONFIG") { + cmd.env_remove(&key); + } + } +} + +async fn run_shell_command( + app_handle: &AppHandle, + provider_id: &str, + command: &str, +) -> Result<(), String> { + let extended_path = build_extended_path(); + + let shell = if cfg!(target_os = "windows") { + "cmd" + } else { + "sh" + }; + let flag = if cfg!(target_os = "windows") { + "/C" + } else { + "-c" + }; + + let mut child_cmd = tokio::process::Command::new(shell); + child_cmd + .arg(flag) + .arg(command) + .env("PATH", &extended_path) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()); + strip_npm_config_env(&mut child_cmd); + + let mut child = child_cmd + .spawn() + .map_err(|e| format!("Failed to start command: {e}"))?; + + let stdout = child.stdout.take(); + let stderr = child.stderr.take(); + + let provider_id_owned = provider_id.to_string(); + let app_for_stdout = app_handle.clone(); + + let stdout_task = tokio::spawn(async move { + if let Some(stdout) = stdout { + let reader = BufReader::new(stdout); + let mut lines = reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + let _ = app_for_stdout.emit( + "agent-setup:output", + AgentSetupOutput { + provider_id: provider_id_owned.clone(), + line, + }, + ); + } + } + }); + + let provider_id_owned2 = provider_id.to_string(); + let app_for_stderr = app_handle.clone(); + + let stderr_task = tokio::spawn(async move { + if let Some(stderr) = stderr { + let reader = BufReader::new(stderr); + let mut lines = reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + let _ = app_for_stderr.emit( + "agent-setup:output", + AgentSetupOutput { + provider_id: provider_id_owned2.clone(), + line, + }, + ); + } + } + }); + + let _ = tokio::join!(stdout_task, stderr_task); + + let status = child + .wait() + .await + .map_err(|e| format!("Failed to wait for command: {e}"))?; + + if status.success() { + Ok(()) + } else { + let code = status.code().unwrap_or(-1); + Err(format!("Command exited with code {code}")) + } +} diff --git a/ui/goose2/src-tauri/src/commands/credentials.rs b/ui/goose2/src-tauri/src/commands/credentials.rs new file mode 100644 index 0000000000..94b063002e --- /dev/null +++ b/ui/goose2/src-tauri/src/commands/credentials.rs @@ -0,0 +1,50 @@ +use tauri::State; + +use crate::services::goose_config::{FieldValue, GooseConfig, ProviderStatus}; +use crate::services::provider_defs::find_config_key; + +#[tauri::command] +pub fn get_provider_config( + config: State<'_, GooseConfig>, + provider_id: String, +) -> Result, String> { + config.get_provider_field_values(&provider_id) +} + +#[tauri::command] +pub fn save_provider_field( + config: State<'_, GooseConfig>, + key: String, + value: String, +) -> Result<(), String> { + let config_key = + find_config_key(&key).ok_or_else(|| format!("Unknown provider config key '{key}'"))?; + let trimmed_value = value.trim(); + if trimmed_value.is_empty() { + return Err("Field value cannot be empty".to_string()); + } + + if config_key.is_secret { + config.set_secret(&key, trimmed_value) + } else { + config.set_param(&key, trimmed_value) + } +} + +#[tauri::command] +pub fn delete_provider_config( + config: State<'_, GooseConfig>, + provider_id: String, +) -> Result<(), String> { + config.delete_all_provider_fields(&provider_id) +} + +#[tauri::command] +pub fn check_all_provider_status(config: State<'_, GooseConfig>) -> Vec { + config.check_all_provider_status() +} + +#[tauri::command] +pub fn restart_app(app: tauri::AppHandle) { + app.restart(); +} diff --git a/ui/goose2/src-tauri/src/commands/mod.rs b/ui/goose2/src-tauri/src/commands/mod.rs index ea5550a143..77a775ee36 100644 --- a/ui/goose2/src-tauri/src/commands/mod.rs +++ b/ui/goose2/src-tauri/src/commands/mod.rs @@ -1,7 +1,10 @@ pub mod acp; +pub mod agent_setup; pub mod agents; +pub mod credentials; pub mod doctor; pub mod git; +pub mod model_setup; pub mod projects; pub mod skills; pub mod system; diff --git a/ui/goose2/src-tauri/src/commands/model_setup.rs b/ui/goose2/src-tauri/src/commands/model_setup.rs new file mode 100644 index 0000000000..bad814ae24 --- /dev/null +++ b/ui/goose2/src-tauri/src/commands/model_setup.rs @@ -0,0 +1,219 @@ +use tauri::{AppHandle, Emitter}; +use tokio::io::{AsyncBufReadExt, BufReader}; + +use crate::services::acp::resolve_goose_binary; + +#[derive(serde::Serialize, Clone)] +#[serde(rename_all = "camelCase")] +struct ModelSetupOutput { + provider_id: String, + line: String, +} + +#[tauri::command] +pub async fn authenticate_model_provider( + app_handle: AppHandle, + provider_id: String, + provider_label: String, +) -> Result<(), String> { + if cfg!(target_os = "windows") { + return Err("Native Goose sign-in is not supported on Windows yet".to_string()); + } + + let goose_binary = resolve_goose_binary()?; + let quoted_label = shell_quote(&provider_label); + let quoted_binary = shell_quote(&goose_binary.to_string_lossy()); + + let command = if cfg!(target_os = "linux") { + format!( + "printf '\\n%s\\n' {quoted_label} | script -qf /dev/null -c '{quoted_binary} configure'", + ) + } else { + format!("printf '\\n%s\\n' {quoted_label} | script -q /dev/null {quoted_binary} configure",) + }; + + run_shell_command(&app_handle, &provider_id, &command).await +} + +fn shell_quote(value: &str) -> String { + format!("'{}'", value.replace('\'', "'\"'\"'")) +} + +fn strip_ansi(value: &str) -> String { + let bytes = value.as_bytes(); + let mut index = 0; + let mut output = String::new(); + + while index < bytes.len() { + if bytes[index] == 0x1b { + index += 1; + if index < bytes.len() && bytes[index] == b'[' { + index += 1; + while index < bytes.len() { + let byte = bytes[index]; + index += 1; + if (0x40..=0x7e).contains(&byte) { + break; + } + } + } + continue; + } + + if bytes[index].is_ascii_control() { + index += 1; + continue; + } + + output.push(bytes[index] as char); + index += 1; + } + + output +} + +fn normalize_output_line(line: &str) -> Option { + let cleaned = strip_ansi(line); + let trimmed = cleaned + .trim() + .trim_start_matches(['│', '┌', '└', '◆', '◇', '●', '○']) + .trim(); + + if trimmed.is_empty() + || trimmed == "Configure Providers" + || trimmed == "What would you like to configure?" + || trimmed == "Which model provider should we use?" + || trimmed == "This will update your existing config files" + || trimmed.starts_with("if you prefer, you can edit them directly at") + { + return None; + } + + Some(trimmed.to_string()) +} + +fn is_relevant_output(line: &str) -> bool { + line.starts_with("Configuring ") + || line.starts_with("Please visit ") + || line.starts_with("Open ") + || line.starts_with("Opening ") + || line.starts_with("Waiting ") + || line.starts_with("Authentication") + || line.starts_with("Authenticated") + || line.starts_with("Saved ") + || line.contains("oauth") + || line.contains("OAuth") + || line.contains("device code") + || line.contains("login/device") + || line.contains("browser") +} + +async fn run_shell_command( + app_handle: &AppHandle, + provider_id: &str, + command: &str, +) -> Result<(), String> { + let shell = if cfg!(target_os = "windows") { + "cmd" + } else { + "sh" + }; + let flag = if cfg!(target_os = "windows") { + "/C" + } else { + "-c" + }; + + let _ = app_handle.emit( + "model-setup:output", + ModelSetupOutput { + provider_id: provider_id.to_string(), + line: "Starting Goose sign-in...".to_string(), + }, + ); + + let mut child = tokio::process::Command::new(shell) + .arg(flag) + .arg(command) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .map_err(|e| format!("Failed to start sign-in flow: {e}"))?; + + let stdout = child.stdout.take(); + let stderr = child.stderr.take(); + + let provider_id_stdout = provider_id.to_string(); + let app_stdout = app_handle.clone(); + + let stdout_task = tokio::spawn(async move { + let mut has_relevant_output = false; + if let Some(stdout) = stdout { + let reader = BufReader::new(stdout); + let mut lines = reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + let Some(line) = normalize_output_line(&line) else { + continue; + }; + + if !has_relevant_output && is_relevant_output(&line) { + has_relevant_output = true; + } + + if has_relevant_output { + let _ = app_stdout.emit( + "model-setup:output", + ModelSetupOutput { + provider_id: provider_id_stdout.clone(), + line, + }, + ); + } + } + } + }); + + let provider_id_stderr = provider_id.to_string(); + let app_stderr = app_handle.clone(); + + let stderr_task = tokio::spawn(async move { + let mut has_relevant_output = false; + if let Some(stderr) = stderr { + let reader = BufReader::new(stderr); + let mut lines = reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + let Some(line) = normalize_output_line(&line) else { + continue; + }; + + if !has_relevant_output && is_relevant_output(&line) { + has_relevant_output = true; + } + + if has_relevant_output { + let _ = app_stderr.emit( + "model-setup:output", + ModelSetupOutput { + provider_id: provider_id_stderr.clone(), + line, + }, + ); + } + } + } + }); + + let _ = tokio::join!(stdout_task, stderr_task); + + let status = child + .wait() + .await + .map_err(|e| format!("Failed to wait for sign-in flow: {e}"))?; + + if status.success() { + Ok(()) + } else { + let code = status.code().unwrap_or(-1); + Err(format!("Goose sign-in exited with code {code}")) + } +} diff --git a/ui/goose2/src-tauri/src/lib.rs b/ui/goose2/src-tauri/src/lib.rs index 13d04762f1..bc23f19a4a 100644 --- a/ui/goose2/src-tauri/src/lib.rs +++ b/ui/goose2/src-tauri/src/lib.rs @@ -5,6 +5,7 @@ mod types; use std::sync::Arc; use services::acp::AcpSessionRegistry; +use services::goose_config::GooseConfig; use services::personas::PersonaStore; use tauri_plugin_window_state::StateFlags; @@ -30,6 +31,7 @@ pub fn run() { .build(), ) .manage(PersonaStore::new()) + .manage(GooseConfig::new()) .manage(acp_registry) .invoke_handler(tauri::generate_handler![ commands::agents::list_personas, @@ -67,6 +69,16 @@ pub fn run() { commands::doctor::run_doctor, commands::doctor::run_doctor_fix, commands::git::get_git_state, + commands::credentials::get_provider_config, + commands::credentials::save_provider_field, + commands::credentials::delete_provider_config, + commands::credentials::check_all_provider_status, + commands::credentials::restart_app, + commands::model_setup::authenticate_model_provider, + commands::agent_setup::check_agent_installed, + commands::agent_setup::check_agent_auth, + commands::agent_setup::install_agent, + commands::agent_setup::authenticate_agent, commands::system::get_home_dir, commands::system::path_exists, ]) diff --git a/ui/goose2/src-tauri/src/services/acp/goose_serve.rs b/ui/goose2/src-tauri/src/services/acp/goose_serve.rs index d14777efc9..5603f7cdaa 100644 --- a/ui/goose2/src-tauri/src/services/acp/goose_serve.rs +++ b/ui/goose2/src-tauri/src/services/acp/goose_serve.rs @@ -158,7 +158,7 @@ fn default_serve_working_dir() -> PathBuf { // Binary resolution // --------------------------------------------------------------------------- -fn resolve_goose_binary() -> Result { +pub(crate) fn resolve_goose_binary() -> Result { let binary_path = if let Ok(override_path) = std::env::var("GOOSE_BIN") { let path = PathBuf::from(&override_path); if !path.exists() { diff --git a/ui/goose2/src-tauri/src/services/acp/mod.rs b/ui/goose2/src-tauri/src/services/acp/mod.rs index 497b782c3b..0dcbfc5dbc 100644 --- a/ui/goose2/src-tauri/src/services/acp/mod.rs +++ b/ui/goose2/src-tauri/src/services/acp/mod.rs @@ -4,6 +4,7 @@ mod payloads; mod registry; mod writer; +pub(crate) use goose_serve::resolve_goose_binary; pub use manager::{AcpSessionInfo, GooseAcpManager}; pub use registry::{AcpRunningSession, AcpSessionRegistry}; pub use writer::TauriMessageWriter; diff --git a/ui/goose2/src-tauri/src/services/goose_config.rs b/ui/goose2/src-tauri/src/services/goose_config.rs new file mode 100644 index 0000000000..b77b154d34 --- /dev/null +++ b/ui/goose2/src-tauri/src/services/goose_config.rs @@ -0,0 +1,452 @@ +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Mutex; + +use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs}; +use serde::Serialize; +use serde_json::Value; + +use super::provider_defs::{find_provider_def, PROVIDER_CONFIG_DEFS}; + +const KEYRING_SERVICE: &str = "goose"; +const KEYRING_USERNAME: &str = "secrets"; +const CONFIG_YAML_NAME: &str = "config.yaml"; +const SECRETS_YAML_NAME: &str = "secrets.yaml"; +const SECRET_MASK_PREFIX_LEN: usize = 4; +const SECRET_MASK_SUFFIX_LEN: usize = 3; +const SECRET_MASK_FALLBACK: &str = "***"; + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProviderStatus { + pub provider_id: String, + pub is_configured: bool, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FieldValue { + pub key: String, + pub value: Option, + pub is_set: bool, + pub is_secret: bool, + pub required: bool, +} + +pub struct GooseConfig { + config_dir: PathBuf, + guard: Mutex<()>, +} + +impl GooseConfig { + pub fn new() -> Self { + let config_dir = Self::resolve_config_dir(); + log::info!("GooseConfig using config dir: {}", config_dir.display()); + Self { + config_dir, + guard: Mutex::new(()), + } + } + + fn resolve_config_dir() -> PathBuf { + if let Ok(root) = std::env::var("GOOSE_PATH_ROOT") { + return PathBuf::from(root).join("config"); + } + + let strategy = choose_app_strategy(AppStrategyArgs { + top_level_domain: "Block".to_string(), + author: "Block".to_string(), + app_name: "goose".to_string(), + }) + .expect("goose requires a home dir"); + + strategy.config_dir() + } + + fn read_config_map(&self) -> serde_yaml::Mapping { + let config_path = self.config_dir.join(CONFIG_YAML_NAME); + match std::fs::read_to_string(&config_path) { + Ok(contents) => { + serde_yaml::from_str::(&contents).unwrap_or_default() + } + Err(_) => serde_yaml::Mapping::new(), + } + } + + fn write_config_map(&self, config: &serde_yaml::Mapping) -> Result<(), String> { + let path = self.config_dir.join(CONFIG_YAML_NAME); + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + + let yaml = serde_yaml::to_string(config) + .map_err(|e| format!("Failed to serialize config: {e}"))?; + + std::fs::write(&path, yaml).map_err(|e| format!("Failed to write config file: {e}")) + } + + fn get_secret(&self, key: &str) -> Option { + let env_key = key.to_uppercase(); + if let Ok(value) = std::env::var(&env_key) { + return Some(value); + } + + let secrets = self.all_secrets(); + secrets + .get(key) + .and_then(|value| value.as_str().map(|secret| secret.to_string())) + } + + fn mask_secret_value(value: &str) -> String { + let prefix: String = value.chars().take(SECRET_MASK_PREFIX_LEN).collect(); + let suffix_chars: Vec = value.chars().rev().take(SECRET_MASK_SUFFIX_LEN).collect(); + let suffix: String = suffix_chars.into_iter().rev().collect(); + + if prefix.is_empty() + || suffix.is_empty() + || value.chars().count() <= SECRET_MASK_PREFIX_LEN + SECRET_MASK_SUFFIX_LEN + { + return SECRET_MASK_FALLBACK.to_string(); + } + + format!("{prefix}...{suffix}") + } + + fn has_param(&self, key: &str) -> bool { + self.get_param(key).is_some() + } + + fn has_secret(&self, key: &str) -> bool { + self.get_secret(key).is_some() + } + + pub fn get_param(&self, key: &str) -> Option { + let env_key = key.to_uppercase(); + if let Ok(value) = std::env::var(&env_key) { + return Some(value); + } + + let config = self.read_config_map(); + let yaml_key = serde_yaml::Value::String(key.to_string()); + config + .get(&yaml_key) + .and_then(|value| value.as_str().map(|param| param.to_string())) + } + + pub fn get_secret_masked(&self, key: &str) -> Option { + self.get_secret(key) + .map(|value| Self::mask_secret_value(&value)) + } + + pub fn set_param(&self, key: &str, value: &str) -> Result<(), String> { + let _guard = self.guard.lock().unwrap(); + + let mut config = self.read_config_map(); + config.insert( + serde_yaml::Value::String(key.to_string()), + serde_yaml::Value::String(value.to_string()), + ); + + self.write_config_map(&config) + } + + pub fn delete_param(&self, key: &str) -> Result { + let _guard = self.guard.lock().unwrap(); + + let mut config = self.read_config_map(); + let yaml_key = serde_yaml::Value::String(key.to_string()); + let removed = config.remove(&yaml_key).is_some(); + + if removed { + self.write_config_map(&config)?; + } + + Ok(removed) + } + + pub fn set_secret(&self, key: &str, value: &str) -> Result<(), String> { + let _guard = self.guard.lock().unwrap(); + + let mut secrets = self.all_secrets(); + secrets.insert(key.to_string(), Value::String(value.to_string())); + + if self.is_keyring_disabled() { + self.write_secrets_to_file(&secrets) + } else { + self.write_secrets_to_keyring(&secrets) + } + } + + pub fn delete_secret(&self, key: &str) -> Result { + let _guard = self.guard.lock().unwrap(); + + let mut secrets = self.all_secrets(); + let removed = secrets.remove(key).is_some(); + + if removed { + if self.is_keyring_disabled() { + self.write_secrets_to_file(&secrets)?; + } else { + self.write_secrets_to_keyring(&secrets)?; + } + } + + Ok(removed) + } + + fn has_oauth_cache(&self, cache_path: &str) -> bool { + let full_path = self.config_dir.join(cache_path); + if full_path.is_dir() { + std::fs::read_dir(&full_path) + .map(|mut entries| entries.any(|_| true)) + .unwrap_or(false) + } else { + full_path.exists() + } + } + + fn delete_oauth_cache(&self, cache_path: &str) -> Result { + let _guard = self.guard.lock().unwrap(); + let full_path = self.config_dir.join(cache_path); + + if full_path.is_dir() { + std::fs::remove_dir_all(&full_path) + .map_err(|e| format!("Failed to remove OAuth cache directory: {e}"))?; + return Ok(true); + } + + if full_path.exists() { + std::fs::remove_file(&full_path) + .map_err(|e| format!("Failed to remove OAuth cache file: {e}"))?; + return Ok(true); + } + + Ok(false) + } + + pub fn check_provider_status(&self, provider_id: &str) -> ProviderStatus { + if provider_id == "databricks" { + let has_host = self.has_param("DATABRICKS_HOST"); + let has_token = self.has_secret("DATABRICKS_TOKEN"); + let has_oauth = self.has_oauth_cache("databricks/oauth"); + + return ProviderStatus { + provider_id: provider_id.to_string(), + is_configured: has_host && (has_token || has_oauth), + }; + } + + let def = match find_provider_def(provider_id) { + Some(def) => def, + None => { + return ProviderStatus { + provider_id: provider_id.to_string(), + is_configured: false, + } + } + }; + + let has_oauth = def + .oauth_cache_path + .map(|p| self.has_oauth_cache(p)) + .unwrap_or(false); + + if has_oauth { + return ProviderStatus { + provider_id: provider_id.to_string(), + is_configured: true, + }; + } + + let all_required_present = def.keys.iter().all(|k| { + if !k.required { + return true; + } + if k.is_secret { + self.has_secret(k.name) + } else { + self.has_param(k.name) + } + }); + + let has_any_key = def.keys.iter().any(|k| { + if k.is_secret { + self.has_secret(k.name) + } else { + self.has_param(k.name) + } + }); + + let is_configured = if def.keys.is_empty() { + false + } else { + all_required_present && has_any_key + }; + + ProviderStatus { + provider_id: provider_id.to_string(), + is_configured, + } + } + + pub fn check_all_provider_status(&self) -> Vec { + PROVIDER_CONFIG_DEFS + .iter() + .map(|def| self.check_provider_status(def.id)) + .collect() + } + + pub fn get_provider_field_values(&self, provider_id: &str) -> Result, String> { + let def = find_provider_def(provider_id) + .ok_or_else(|| format!("Unknown provider '{provider_id}'"))?; + + Ok(def + .keys + .iter() + .map(|config_key| { + let value = if config_key.is_secret { + self.get_secret_masked(config_key.name) + } else { + self.get_param(config_key.name) + }; + + FieldValue { + key: config_key.name.to_string(), + is_set: value.is_some(), + value, + is_secret: config_key.is_secret, + required: config_key.required, + } + }) + .collect()) + } + + pub fn delete_all_provider_fields(&self, provider_id: &str) -> Result<(), String> { + let def = find_provider_def(provider_id) + .ok_or_else(|| format!("Unknown provider '{provider_id}'"))?; + + for config_key in def.keys { + if config_key.is_secret { + self.delete_secret(config_key.name)?; + } else { + self.delete_param(config_key.name)?; + } + } + + if let Some(oauth_cache_path) = def.oauth_cache_path { + self.delete_oauth_cache(oauth_cache_path)?; + } + + Ok(()) + } + + fn is_keyring_disabled(&self) -> bool { + if std::env::var("GOOSE_DISABLE_KEYRING").is_ok() { + return true; + } + + let config_path = self.config_dir.join(CONFIG_YAML_NAME); + if let Ok(contents) = std::fs::read_to_string(&config_path) { + if let Ok(yaml) = serde_yaml::from_str::(&contents) { + let key = serde_yaml::Value::String("GOOSE_DISABLE_KEYRING".to_string()); + if let Some(val) = yaml.get(&key) { + return val.as_bool().unwrap_or(false) + || val + .as_str() + .map(|s| s == "true" || s == "1") + .unwrap_or(false); + } + } + } + + false + } + + fn all_secrets(&self) -> HashMap { + if self.is_keyring_disabled() { + return self.read_secrets_from_file(); + } + + match self.read_secrets_from_keyring() { + Ok(secrets) => secrets, + Err(e) => { + log::warn!("Keyring read failed, falling back to secrets file: {e}"); + self.read_secrets_from_file() + } + } + } + + fn read_secrets_from_keyring(&self) -> Result, String> { + let entry = keyring::Entry::new(KEYRING_SERVICE, KEYRING_USERNAME) + .map_err(|e| format!("Failed to access keyring: {e}"))?; + + match entry.get_password() { + Ok(json_str) => serde_json::from_str(&json_str) + .map_err(|e| format!("Failed to parse keyring JSON: {e}")), + Err(keyring::Error::NoEntry) => Ok(HashMap::new()), + Err(e) => Err(format!("Failed to read keyring: {e}")), + } + } + + fn read_secrets_from_file(&self) -> HashMap { + let path = self.config_dir.join(SECRETS_YAML_NAME); + match std::fs::read_to_string(&path) { + Ok(contents) => serde_yaml::from_str::>(&contents) + .unwrap_or_default() + .into_iter() + .map(|(k, v)| (k, Value::String(v))) + .collect(), + Err(_) => HashMap::new(), + } + } + + fn write_secrets_to_keyring(&self, secrets: &HashMap) -> Result<(), String> { + let entry = keyring::Entry::new(KEYRING_SERVICE, KEYRING_USERNAME) + .map_err(|e| format!("Failed to access keyring: {e}"))?; + + let json_str = serde_json::to_string(secrets) + .map_err(|e| format!("Failed to serialize secrets: {e}"))?; + + entry + .set_password(&json_str) + .map_err(|e| format!("Failed to write to keyring: {e}")) + } + + fn write_secrets_to_file(&self, secrets: &HashMap) -> Result<(), String> { + let path = self.config_dir.join(SECRETS_YAML_NAME); + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + + let yaml_map: HashMap = secrets + .iter() + .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) + .collect(); + + let yaml = serde_yaml::to_string(&yaml_map) + .map_err(|e| format!("Failed to serialize secrets: {e}"))?; + + #[cfg(unix)] + { + use std::fs::OpenOptions; + use std::io::Write; + use std::os::unix::fs::OpenOptionsExt; + + let mut file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o600) + .open(&path) + .map_err(|e| format!("Failed to open secrets file: {e}"))?; + + file.write_all(yaml.as_bytes()) + .map_err(|e| format!("Failed to write secrets file: {e}")) + } + + #[cfg(not(unix))] + { + std::fs::write(&path, yaml).map_err(|e| format!("Failed to write secrets file: {e}")) + } + } +} diff --git a/ui/goose2/src-tauri/src/services/mod.rs b/ui/goose2/src-tauri/src/services/mod.rs index c0b447c6b4..77759b3ac4 100644 --- a/ui/goose2/src-tauri/src/services/mod.rs +++ b/ui/goose2/src-tauri/src/services/mod.rs @@ -1,2 +1,4 @@ pub mod acp; +pub mod goose_config; pub mod personas; +pub(crate) mod provider_defs; diff --git a/ui/goose2/src-tauri/src/services/provider_defs.rs b/ui/goose2/src-tauri/src/services/provider_defs.rs new file mode 100644 index 0000000000..0a2a326eaf --- /dev/null +++ b/ui/goose2/src-tauri/src/services/provider_defs.rs @@ -0,0 +1,139 @@ +pub(crate) struct ConfigKey { + pub name: &'static str, + pub is_secret: bool, + pub required: bool, +} + +pub(crate) struct ProviderConfigDef { + pub id: &'static str, + pub keys: &'static [ConfigKey], + pub oauth_cache_path: Option<&'static str>, +} + +const fn key(name: &'static str, is_secret: bool, required: bool) -> ConfigKey { + ConfigKey { + name, + is_secret, + required, + } +} + +pub(crate) static PROVIDER_CONFIG_DEFS: &[ProviderConfigDef] = &[ + ProviderConfigDef { + id: "anthropic", + keys: &[key("ANTHROPIC_API_KEY", true, true)], + oauth_cache_path: None, + }, + ProviderConfigDef { + id: "openai", + keys: &[key("OPENAI_API_KEY", true, true)], + oauth_cache_path: None, + }, + ProviderConfigDef { + id: "google", + keys: &[key("GOOGLE_API_KEY", true, true)], + oauth_cache_path: None, + }, + ProviderConfigDef { + id: "openrouter", + keys: &[key("OPENROUTER_API_KEY", true, true)], + oauth_cache_path: None, + }, + ProviderConfigDef { + id: "xai", + keys: &[key("XAI_API_KEY", true, true)], + oauth_cache_path: None, + }, + ProviderConfigDef { + id: "nanogpt", + keys: &[key("NANOGPT_API_KEY", true, true)], + oauth_cache_path: None, + }, + ProviderConfigDef { + id: "venice", + keys: &[key("VENICE_API_KEY", true, true)], + oauth_cache_path: None, + }, + ProviderConfigDef { + id: "tetrate", + keys: &[key("TETRATE_API_KEY", true, true)], + oauth_cache_path: None, + }, + ProviderConfigDef { + id: "databricks", + keys: &[ + key("DATABRICKS_HOST", false, true), + key("DATABRICKS_TOKEN", true, false), + ], + oauth_cache_path: Some("databricks/oauth"), + }, + ProviderConfigDef { + id: "snowflake", + keys: &[ + key("SNOWFLAKE_HOST", false, true), + key("SNOWFLAKE_TOKEN", true, true), + ], + oauth_cache_path: None, + }, + ProviderConfigDef { + id: "litellm", + keys: &[ + key("LITELLM_HOST", false, true), + key("LITELLM_API_KEY", true, false), + ], + oauth_cache_path: None, + }, + ProviderConfigDef { + id: "azure", + keys: &[ + key("AZURE_OPENAI_ENDPOINT", false, true), + key("AZURE_OPENAI_DEPLOYMENT_NAME", false, true), + key("AZURE_OPENAI_API_KEY", true, false), + ], + oauth_cache_path: None, + }, + ProviderConfigDef { + id: "bedrock", + keys: &[key("AWS_REGION", false, false)], + oauth_cache_path: None, + }, + ProviderConfigDef { + id: "gcp_vertex_ai", + keys: &[ + key("GCP_PROJECT_ID", false, true), + key("GCP_LOCATION", false, true), + ], + oauth_cache_path: None, + }, + ProviderConfigDef { + id: "chatgpt_codex", + keys: &[key("CHATGPT_CODEX_TOKEN", true, true)], + oauth_cache_path: None, + }, + ProviderConfigDef { + id: "github_copilot", + keys: &[], + oauth_cache_path: Some("githubcopilot/info.json"), + }, + ProviderConfigDef { + id: "ollama", + keys: &[], + oauth_cache_path: None, + }, + ProviderConfigDef { + id: "local_inference", + keys: &[], + oauth_cache_path: None, + }, +]; + +pub(crate) fn find_config_key(key_name: &str) -> Option<&'static ConfigKey> { + PROVIDER_CONFIG_DEFS + .iter() + .flat_map(|def| def.keys.iter()) + .find(|key| key.name == key_name) +} + +pub(crate) fn find_provider_def(provider_id: &str) -> Option<&'static ProviderConfigDef> { + PROVIDER_CONFIG_DEFS.iter().find(|d| d.id == provider_id) +} diff --git a/ui/goose2/src/features/providers/api/agentSetup.ts b/ui/goose2/src/features/providers/api/agentSetup.ts new file mode 100644 index 0000000000..2eec9954c4 --- /dev/null +++ b/ui/goose2/src/features/providers/api/agentSetup.ts @@ -0,0 +1,36 @@ +import { invoke } from "@tauri-apps/api/core"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; + +interface AgentSetupOutput { + providerId: string; + line: string; +} + +export async function checkAgentInstalled( + providerId: string, +): Promise { + return invoke("check_agent_installed", { providerId }); +} + +export async function checkAgentAuth(providerId: string): Promise { + return invoke("check_agent_auth", { providerId }); +} + +export async function installAgent(providerId: string): Promise { + return invoke("install_agent", { providerId }); +} + +export async function authenticateAgent(providerId: string): Promise { + return invoke("authenticate_agent", { providerId }); +} + +export function onAgentSetupOutput( + providerId: string, + callback: (line: string) => void, +): Promise { + return listen("agent-setup:output", (event) => { + if (event.payload.providerId === providerId) { + callback(event.payload.line); + } + }); +} diff --git a/ui/goose2/src/features/providers/api/credentials.ts b/ui/goose2/src/features/providers/api/credentials.ts new file mode 100644 index 0000000000..2497fcf6c5 --- /dev/null +++ b/ui/goose2/src/features/providers/api/credentials.ts @@ -0,0 +1,32 @@ +import { invoke } from "@tauri-apps/api/core"; +import type { ProviderFieldValue } from "@/shared/types/providers"; + +export interface ProviderStatus { + providerId: string; + isConfigured: boolean; +} + +export async function getProviderConfig( + providerId: string, +): Promise { + return invoke("get_provider_config", { providerId }); +} + +export async function saveProviderField( + key: string, + value: string, +): Promise { + return invoke("save_provider_field", { key, value }); +} + +export async function deleteProviderConfig(providerId: string): Promise { + return invoke("delete_provider_config", { providerId }); +} + +export async function checkAllProviderStatus(): Promise { + return invoke("check_all_provider_status"); +} + +export async function restartApp(): Promise { + return invoke("restart_app"); +} diff --git a/ui/goose2/src/features/providers/api/modelSetup.ts b/ui/goose2/src/features/providers/api/modelSetup.ts new file mode 100644 index 0000000000..f24da26105 --- /dev/null +++ b/ui/goose2/src/features/providers/api/modelSetup.ts @@ -0,0 +1,25 @@ +import { invoke } from "@tauri-apps/api/core"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; + +interface ModelSetupOutput { + providerId: string; + line: string; +} + +export async function authenticateModelProvider( + providerId: string, + providerLabel: string, +): Promise { + return invoke("authenticate_model_provider", { providerId, providerLabel }); +} + +export function onModelSetupOutput( + providerId: string, + callback: (line: string) => void, +): Promise { + return listen("model-setup:output", (event) => { + if (event.payload.providerId === providerId) { + callback(event.payload.line); + } + }); +} diff --git a/ui/goose2/src/features/providers/hooks/useCredentials.ts b/ui/goose2/src/features/providers/hooks/useCredentials.ts new file mode 100644 index 0000000000..8b7c73b827 --- /dev/null +++ b/ui/goose2/src/features/providers/hooks/useCredentials.ts @@ -0,0 +1,100 @@ +import { useState, useEffect, useCallback, useMemo } from "react"; +import { + getProviderConfig, + saveProviderField, + deleteProviderConfig, + type ProviderStatus, + checkAllProviderStatus, + restartApp, +} from "@/features/providers/api/credentials"; +import type { ProviderFieldValue } from "@/shared/types/providers"; + +interface UseCredentialsReturn { + configuredIds: Set; + loading: boolean; + saving: boolean; + needsRestart: boolean; + getConfig: (providerId: string) => Promise; + save: (key: string, value: string) => Promise; + remove: (providerId: string) => Promise; + restart: () => Promise; + completeNativeSetup: () => Promise; +} + +export function useCredentials(): UseCredentialsReturn { + const [statuses, setStatuses] = useState([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [needsRestart, setNeedsRestart] = useState(false); + + const refreshStatuses = useCallback(async () => { + const nextStatuses = await checkAllProviderStatus(); + setStatuses(nextStatuses); + return nextStatuses; + }, []); + + useEffect(() => { + refreshStatuses() + .catch(() => {}) + .finally(() => setLoading(false)); + }, [refreshStatuses]); + + const configuredIds = useMemo( + () => + new Set(statuses.filter((s) => s.isConfigured).map((s) => s.providerId)), + [statuses], + ); + + const getConfig = useCallback(async (providerId: string) => { + return getProviderConfig(providerId); + }, []); + + const save = useCallback( + async (key: string, value: string) => { + setSaving(true); + try { + await saveProviderField(key, value); + await refreshStatuses(); + setNeedsRestart(true); + } finally { + setSaving(false); + } + }, + [refreshStatuses], + ); + + const remove = useCallback( + async (providerId: string) => { + setSaving(true); + try { + await deleteProviderConfig(providerId); + await refreshStatuses(); + setNeedsRestart(true); + } finally { + setSaving(false); + } + }, + [refreshStatuses], + ); + + const restart = useCallback(async () => { + await restartApp(); + }, []); + + const completeNativeSetup = useCallback(async () => { + await refreshStatuses(); + setNeedsRestart(true); + }, [refreshStatuses]); + + return { + configuredIds, + loading, + saving, + needsRestart, + getConfig, + save, + remove, + restart, + completeNativeSetup, + }; +} diff --git a/ui/goose2/src/features/providers/providerCatalog.ts b/ui/goose2/src/features/providers/providerCatalog.ts new file mode 100644 index 0000000000..dac83352d6 --- /dev/null +++ b/ui/goose2/src/features/providers/providerCatalog.ts @@ -0,0 +1,433 @@ +import type { ProviderCatalogEntry } from "@/shared/types/providers"; + +export const PROVIDER_CATALOG: ProviderCatalogEntry[] = [ + // ── Agent providers ────────────────────────────────────────────── + { + id: "goose", + displayName: "Goose", + category: "agent", + description: "Block's open-source coding agent", + setupMethod: "none", + tier: "promoted", + }, + { + id: "claude-acp", + displayName: "Claude Code", + category: "agent", + description: "Anthropic's agentic coding tool", + setupMethod: "cli_auth", + binaryName: "claude-agent-acp", + installCommand: + "npm install -g @anthropic-ai/claude-code @zed-industries/claude-agent-acp", + authCommand: "claude auth login", + authStatusCommand: "claude auth status", + docsUrl: "https://docs.anthropic.com/en/docs/claude-code", + tier: "promoted", + }, + { + id: "codex-acp", + displayName: "Codex", + category: "agent", + description: "OpenAI's coding agent", + setupMethod: "cli_auth", + binaryName: "codex-acp", + installCommand: "npm install -g @openai/codex @zed-industries/codex-acp", + authCommand: "codex login", + authStatusCommand: "codex login status", + docsUrl: "https://github.com/openai/codex", + tier: "promoted", + }, + { + id: "copilot-acp", + displayName: "GitHub Copilot", + category: "agent", + description: "GitHub's AI pair programmer", + setupMethod: "cli_auth", + binaryName: "copilot", + installCommand: "npm install -g @github/copilot", + authCommand: "copilot login", + docsUrl: "https://docs.github.com/en/copilot/github-copilot-in-the-cli", + tier: "promoted", + }, + { + id: "amp-acp", + displayName: "Amp", + category: "agent", + description: "Sourcegraph's coding agent", + setupMethod: "cli_auth", + binaryName: "amp-acp", + installCommand: "npm install -g @sourcegraph/amp@latest amp-acp", + authCommand: "amp login", + authStatusCommand: "amp usage", + docsUrl: "https://ampcode.com", + tier: "standard", + }, + { + id: "cursor-agent", + displayName: "Cursor Agent", + category: "agent", + description: "Cursor's AI agent", + setupMethod: "cli_auth", + binaryName: "cursor-agent", + installCommand: "curl -fsSL https://cursor.com/install | bash", + authCommand: "cursor-agent login", + authStatusCommand: "cursor-agent status", + docsUrl: "https://docs.cursor.com/en/cli/overview", + tier: "standard", + }, + { + id: "pi-acp", + displayName: "Pi", + category: "agent", + description: "Open-source AI coding agent", + setupMethod: "cli_auth", + binaryName: "pi-acp", + docsUrl: "https://github.com/badlogic/pi-mono", + tier: "standard", + showOnlyWhenInstalled: true, + }, + + // ── Model providers (power Goose) ──────────────────────────────── + { + id: "anthropic", + displayName: "Anthropic", + category: "model", + description: "Claude models", + setupMethod: "single_api_key", + envVar: "ANTHROPIC_API_KEY", + fields: [ + { + key: "ANTHROPIC_API_KEY", + label: "API Key", + secret: true, + required: true, + placeholder: "Paste your API key", + }, + ], + docsUrl: "https://console.anthropic.com/settings/keys", + tier: "promoted", + }, + { + id: "google", + displayName: "Google Gemini", + category: "model", + description: "Gemini models", + setupMethod: "single_api_key", + envVar: "GOOGLE_API_KEY", + fields: [ + { + key: "GOOGLE_API_KEY", + label: "API Key", + secret: true, + required: true, + placeholder: "Paste your API key", + }, + ], + docsUrl: "https://aistudio.google.com/apikey", + tier: "promoted", + }, + { + id: "chatgpt_codex", + displayName: "ChatGPT Codex", + category: "model", + description: "OpenAI via ChatGPT subscription", + setupMethod: "oauth_device_code", + nativeConnectQuery: "ChatGPT Codex", + docsUrl: "https://chatgpt.com", + tier: "standard", + }, + { + id: "openai", + displayName: "OpenAI", + category: "model", + description: "GPT and o-series models", + setupMethod: "config_fields", + envVar: "OPENAI_API_KEY", + fields: [ + { + key: "OPENAI_API_KEY", + label: "API Key", + secret: true, + required: true, + placeholder: "Paste your API key", + }, + ], + docsUrl: "https://platform.openai.com/api-keys", + tier: "promoted", + }, + { + id: "ollama", + displayName: "Ollama", + category: "model", + description: "Run models locally", + setupMethod: "local", + docsUrl: "https://ollama.com", + tier: "promoted", + }, + { + id: "openrouter", + displayName: "OpenRouter", + category: "model", + description: "Unified API for many models", + setupMethod: "single_api_key", + envVar: "OPENROUTER_API_KEY", + fields: [ + { + key: "OPENROUTER_API_KEY", + label: "API Key", + secret: true, + required: true, + placeholder: "Paste your API key", + }, + ], + docsUrl: "https://openrouter.ai/keys", + tier: "promoted", + }, + { + id: "databricks", + displayName: "Databricks", + category: "model", + description: "Databricks Foundation Models", + setupMethod: "host_with_oauth_fallback", + fields: [ + { + key: "DATABRICKS_HOST", + label: "Host URL", + secret: false, + required: true, + placeholder: "https://dbc-...cloud.databricks.com", + }, + { + key: "DATABRICKS_TOKEN", + label: "Access Token", + secret: true, + required: false, + placeholder: "Paste your access token", + }, + ], + tier: "standard", + }, + { + id: "github_copilot", + displayName: "GitHub Copilot Models", + category: "model", + description: "Models via GitHub Copilot subscription", + setupMethod: "oauth_device_code", + nativeConnectQuery: "GitHub Copilot", + tier: "standard", + }, + { + id: "xai", + displayName: "xAI", + category: "model", + description: "Grok models", + setupMethod: "single_api_key", + envVar: "XAI_API_KEY", + fields: [ + { + key: "XAI_API_KEY", + label: "API Key", + secret: true, + required: true, + placeholder: "Paste your API key", + }, + ], + tier: "standard", + }, + { + id: "azure", + displayName: "Azure OpenAI", + category: "model", + description: "OpenAI models on Azure", + setupMethod: "config_fields", + fields: [ + { + key: "AZURE_OPENAI_ENDPOINT", + label: "Endpoint", + secret: false, + required: true, + placeholder: "https://your-resource.openai.azure.com", + }, + { + key: "AZURE_OPENAI_DEPLOYMENT_NAME", + label: "Deployment", + secret: false, + required: true, + placeholder: "gpt-4o", + }, + { + key: "AZURE_OPENAI_API_KEY", + label: "API Key", + secret: true, + required: false, + placeholder: "Paste your API key", + }, + ], + tier: "advanced", + }, + { + id: "bedrock", + displayName: "AWS Bedrock", + category: "model", + description: "Models on AWS", + setupMethod: "cloud_credentials", + fields: [ + { + key: "AWS_REGION", + label: "AWS Region", + secret: false, + required: false, + placeholder: "us-west-2", + }, + ], + tier: "advanced", + }, + { + id: "gcp_vertex_ai", + displayName: "GCP Vertex AI", + category: "model", + description: "Models on Google Cloud", + setupMethod: "cloud_credentials", + fields: [ + { + key: "GCP_PROJECT_ID", + label: "Project ID", + secret: false, + required: true, + placeholder: "my-gcp-project", + }, + { + key: "GCP_LOCATION", + label: "Location", + secret: false, + required: true, + placeholder: "us-central1", + }, + ], + tier: "advanced", + }, + { + id: "litellm", + displayName: "LiteLLM", + category: "model", + description: "LiteLLM proxy gateway", + setupMethod: "config_fields", + envVar: "LITELLM_API_KEY", + fields: [ + { + key: "LITELLM_HOST", + label: "Host URL", + secret: false, + required: true, + placeholder: "https://your-proxy.example.com", + }, + { + key: "LITELLM_API_KEY", + label: "API Key", + secret: true, + required: false, + placeholder: "Paste your API key", + }, + ], + tier: "advanced", + }, + { + id: "nanogpt", + displayName: "NanoGPT", + category: "model", + description: "NanoGPT inference", + setupMethod: "single_api_key", + envVar: "NANOGPT_API_KEY", + fields: [ + { + key: "NANOGPT_API_KEY", + label: "API Key", + secret: true, + required: true, + placeholder: "Paste your API key", + }, + ], + tier: "advanced", + }, + { + id: "tetrate", + displayName: "Tetrate", + category: "model", + description: "Tetrate AI gateway", + setupMethod: "single_api_key", + fields: [ + { + key: "TETRATE_API_KEY", + label: "API Key", + secret: true, + required: true, + placeholder: "Paste your API key", + }, + ], + tier: "advanced", + }, + { + id: "venice", + displayName: "Venice", + category: "model", + description: "Venice AI", + setupMethod: "single_api_key", + envVar: "VENICE_API_KEY", + fields: [ + { + key: "VENICE_API_KEY", + label: "API Key", + secret: true, + required: true, + placeholder: "Paste your API key", + }, + ], + tier: "advanced", + }, + { + id: "snowflake", + displayName: "Snowflake", + category: "model", + description: "Snowflake Cortex", + setupMethod: "config_fields", + fields: [ + { + key: "SNOWFLAKE_HOST", + label: "Host URL", + secret: false, + required: true, + placeholder: "https://your-account.snowflakecomputing.com", + }, + { + key: "SNOWFLAKE_TOKEN", + label: "Access Token", + secret: true, + required: true, + placeholder: "Paste your access token", + }, + ], + tier: "advanced", + }, + { + id: "local_inference", + displayName: "Local Inference", + category: "model", + description: "Custom local model server", + setupMethod: "local", + tier: "advanced", + }, +]; + +export function getCatalogEntry( + providerId: string, +): ProviderCatalogEntry | undefined { + return PROVIDER_CATALOG.find((p) => p.id === providerId); +} + +export function getAgentProviders(): ProviderCatalogEntry[] { + return PROVIDER_CATALOG.filter((p) => p.category === "agent"); +} + +export function getModelProviders(): ProviderCatalogEntry[] { + return PROVIDER_CATALOG.filter((p) => p.category === "model"); +} diff --git a/ui/goose2/src/shared/types/providers.ts b/ui/goose2/src/shared/types/providers.ts new file mode 100644 index 0000000000..1c11da26df --- /dev/null +++ b/ui/goose2/src/shared/types/providers.ts @@ -0,0 +1,61 @@ +export type ProviderCategory = "agent" | "model"; + +export type ProviderSetupMethod = + | "none" + | "single_api_key" + | "config_fields" + | "host_with_oauth_fallback" + | "oauth_browser" + | "oauth_device_code" + | "cloud_credentials" + | "local" + | "cli_auth"; + +export type ProviderTier = "promoted" | "standard" | "advanced"; + +export interface ProviderField { + key: string; + label: string; + secret: boolean; + required: boolean; + placeholder?: string; +} + +export interface ProviderFieldValue { + key: string; + value: string | null; + isSet: boolean; + isSecret: boolean; + required: boolean; +} + +export interface ProviderCatalogEntry { + id: string; + displayName: string; + category: ProviderCategory; + description: string; + setupMethod: ProviderSetupMethod; + nativeConnectQuery?: string; + envVar?: string; + fields?: ProviderField[]; + binaryName?: string; + installCommand?: string; + authCommand?: string; + authStatusCommand?: string; + docsUrl?: string; + tier: ProviderTier; + showOnlyWhenInstalled?: boolean; +} + +export type ProviderSetupStatus = + | "built_in" + | "connected" + | "not_installed" + | "not_configured" + | "installing" + | "authenticating" + | "error"; + +export interface ProviderDisplayInfo extends ProviderCatalogEntry { + status: ProviderSetupStatus; +}