add provider setup infrastructure (#130)

* feat: add provider setup infrastructure

Add the backend commands, config services, shared types, and frontend API layer needed for provider setup and credential management.

* fix: harden provider setup commands and config deletion

Resolve PR review issues by deriving provider behavior from backend definitions, preventing renderer-supplied agent commands, and clearing OAuth cache artifacts when provider config is removed.

* fix: expose provider defs within backend

Restore backend compilation by making provider definitions visible to sibling modules that derive secure config behavior from the shared provider metadata.

* refactor: streamline checkAgentAuth function signature

Simplify the function signature of checkAgentAuth in agentSetup.ts by removing unnecessary line breaks, enhancing code readability.
This commit is contained in:
morgmart
2026-04-08 11:53:55 -07:00
committed by GitHub
parent 07b278f53b
commit d9ed14bf7a
18 changed files with 2394 additions and 15 deletions
+509 -14
View File
@@ -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]]
+10
View File
@@ -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"
@@ -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<PathBuf> = 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<bool, String> {
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<bool, String> {
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}"))
}
}
@@ -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<Vec<FieldValue>, 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<ProviderStatus> {
config.check_all_provider_status()
}
#[tauri::command]
pub fn restart_app(app: tauri::AppHandle) {
app.restart();
}
+3
View File
@@ -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;
@@ -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<String> {
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}"))
}
}
+12
View File
@@ -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,
])
@@ -158,7 +158,7 @@ fn default_serve_working_dir() -> PathBuf {
// Binary resolution
// ---------------------------------------------------------------------------
fn resolve_goose_binary() -> Result<PathBuf, String> {
pub(crate) fn resolve_goose_binary() -> Result<PathBuf, String> {
let binary_path = if let Ok(override_path) = std::env::var("GOOSE_BIN") {
let path = PathBuf::from(&override_path);
if !path.exists() {
@@ -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;
@@ -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<String>,
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::<serde_yaml::Mapping>(&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<String> {
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<char> = 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<String> {
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<String> {
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<bool, String> {
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<bool, String> {
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<bool, String> {
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<ProviderStatus> {
PROVIDER_CONFIG_DEFS
.iter()
.map(|def| self.check_provider_status(def.id))
.collect()
}
pub fn get_provider_field_values(&self, provider_id: &str) -> Result<Vec<FieldValue>, 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::<serde_yaml::Mapping>(&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<String, Value> {
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<HashMap<String, Value>, 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<String, Value> {
let path = self.config_dir.join(SECRETS_YAML_NAME);
match std::fs::read_to_string(&path) {
Ok(contents) => serde_yaml::from_str::<HashMap<String, String>>(&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<String, Value>) -> 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<String, Value>) -> 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<String, String> = 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}"))
}
}
}
+2
View File
@@ -1,2 +1,4 @@
pub mod acp;
pub mod goose_config;
pub mod personas;
pub(crate) mod provider_defs;
@@ -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)
}
@@ -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<boolean> {
return invoke("check_agent_installed", { providerId });
}
export async function checkAgentAuth(providerId: string): Promise<boolean> {
return invoke("check_agent_auth", { providerId });
}
export async function installAgent(providerId: string): Promise<void> {
return invoke("install_agent", { providerId });
}
export async function authenticateAgent(providerId: string): Promise<void> {
return invoke("authenticate_agent", { providerId });
}
export function onAgentSetupOutput(
providerId: string,
callback: (line: string) => void,
): Promise<UnlistenFn> {
return listen<AgentSetupOutput>("agent-setup:output", (event) => {
if (event.payload.providerId === providerId) {
callback(event.payload.line);
}
});
}
@@ -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<ProviderFieldValue[]> {
return invoke("get_provider_config", { providerId });
}
export async function saveProviderField(
key: string,
value: string,
): Promise<void> {
return invoke("save_provider_field", { key, value });
}
export async function deleteProviderConfig(providerId: string): Promise<void> {
return invoke("delete_provider_config", { providerId });
}
export async function checkAllProviderStatus(): Promise<ProviderStatus[]> {
return invoke("check_all_provider_status");
}
export async function restartApp(): Promise<void> {
return invoke("restart_app");
}
@@ -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<void> {
return invoke("authenticate_model_provider", { providerId, providerLabel });
}
export function onModelSetupOutput(
providerId: string,
callback: (line: string) => void,
): Promise<UnlistenFn> {
return listen<ModelSetupOutput>("model-setup:output", (event) => {
if (event.payload.providerId === providerId) {
callback(event.payload.line);
}
});
}
@@ -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<string>;
loading: boolean;
saving: boolean;
needsRestart: boolean;
getConfig: (providerId: string) => Promise<ProviderFieldValue[]>;
save: (key: string, value: string) => Promise<void>;
remove: (providerId: string) => Promise<void>;
restart: () => Promise<void>;
completeNativeSetup: () => Promise<void>;
}
export function useCredentials(): UseCredentialsReturn {
const [statuses, setStatuses] = useState<ProviderStatus[]>([]);
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,
};
}
@@ -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");
}
+61
View File
@@ -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;
}