23 Commits

Author SHA1 Message Date
Dark-Alex-17 3df590f276 fix: sbx isn't copying base files in their respective directories
CI / All (ubuntu-latest) (push) Failing after 25s
CI / All (macos-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-07-01 18:44:07 -06:00
Dark-Alex-17 91300c16fe fix: Update deprecated sbx kit config 2026-07-01 17:52:04 -06:00
Dark-Alex-17 52356ead6c fix: Properly chown the coyote config recursively and password file in the sbx
CI / All (ubuntu-latest) (push) Failing after 25s
CI / All (macos-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-07-01 17:12:42 -06:00
Dark-Alex-17 ad9fc524d4 feat: Pin specific usql version to sbx kit 2026-07-01 17:11:27 -06:00
Dark-Alex-17 af50909a89 feat: recursively take ownership over the copied in coyote config for the sbx 2026-07-01 16:54:57 -06:00
Dark-Alex-17 318d9ba1cd feat: explicitly specify the COYOTE_CONFIG_DIR in the sbx kit 2026-07-01 16:54:42 -06:00
Dark-Alex-17 45d709f28e Merge branch 'main' of github.com:Dark-Alex-17/coyote
CI / All (ubuntu-latest) (push) Failing after 25s
CI / All (macos-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-06-30 14:55:08 -06:00
Dark-Alex-17 9cd074cb9b build: fixed coyote install scripts 2026-06-30 14:54:59 -06:00
Dark-Alex-17 93eec45473 docs: fixed coyote install script URLs 2026-06-30 14:43:00 -06:00
Dark-Alex-17 e585e0b049 feat: --tail-logs can track log rollovers and incoporates a sleep timer to minimize idle CPU cycles
CI / All (ubuntu-latest) (push) Failing after 25s
CI / All (macos-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-06-25 14:01:38 -06:00
Dark-Alex-17 13bfaf9aca feat: Added support for log rolling so log files don't just blow up over time 2026-06-25 13:57:15 -06:00
github-actions[bot] 040dad05d2 chore: bump Cargo.toml to 0.7.3
CI / All (ubuntu-latest) (push) Failing after 24s
CI / All (macos-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-06-24 18:20:20 +00:00
github-actions[bot] 1ba38860f2 bump: version 0.7.2 → 0.7.3 [skip ci] 2026-06-24 18:20:16 +00:00
Dark-Alex-17 84ec5fe7b8 fix: apply bootstrapping of functions at startup to fix edge case
CI / All (ubuntu-latest) (push) Failing after 24s
CI / All (macos-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-06-24 12:13:55 -06:00
github-actions[bot] 1684788fe6 bump: version 0.7.1 → 0.7.2 [skip ci] 2026-06-19 18:51:49 +00:00
Dark-Alex-17 4b7e242998 fix: usql version upgrade
CI / All (ubuntu-latest) (push) Failing after 24s
CI / All (macos-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-06-19 12:45:41 -06:00
github-actions[bot] f69aba2dd8 chore: bump Cargo.toml to 0.7.1
CI / All (ubuntu-latest) (push) Failing after 25s
CI / All (macos-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-06-19 18:04:58 +00:00
github-actions[bot] c3487ecd0e bump: version 0.7.0 → 0.7.1 [skip ci] 2026-06-19 18:04:56 +00:00
Dark-Alex-17 db75391fb6 Merge branch 'main' of github.com:Dark-Alex-17/coyote
CI / All (ubuntu-latest) (push) Failing after 24s
CI / All (macos-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-06-19 11:44:28 -06:00
Dark-Alex-17 e3815af69b fix: sbx mixins must be passed in directories, not as files and the files must be named spec.yaml per new sbx version
CI / All (ubuntu-latest) (push) Failing after 25s
CI / All (macos-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-06-19 11:44:16 -06:00
github-actions[bot] 66a485f924 chore: bump Cargo.toml to 0.7.0 2026-06-18 22:40:24 +00:00
github-actions[bot] 49d7204f89 bump: version 0.6.0 → 0.7.0 [skip ci] 2026-06-18 22:40:19 +00:00
Dark-Alex-17 bbcae1fc2b feat: added configurable cache path via the COYOTE_CACHE_PATH environment variable
CI / All (ubuntu-latest) (push) Failing after 24s
CI / All (macos-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-06-18 16:30:24 -06:00
13 changed files with 595 additions and 193 deletions
+19
View File
@@ -1,7 +1,26 @@
## v0.7.3 (2026-06-24)
### Fix
- apply bootstrapping of functions at startup to fix edge case
## v0.7.2 (2026-06-19)
### Fix
- usql version upgrade
## v0.7.1 (2026-06-19)
### Fix
- sbx mixins must be passed in directories, not as files and the files must be named spec.yaml per new sbx version
## v0.7.0 (2026-06-18)
### Feat
- added configurable cache path via the COYOTE_CACHE_PATH environment variable
- added a memory option to .set tab completions
- Added a diagnostic .info tools subcommand to make it easier to see what tools are enabled in all contexts
- Added additional info outputs for enabled skills and sbx directories
Generated
+104 -65
View File
@@ -566,7 +566,7 @@ dependencies = [
"hyper-util",
"pin-project-lite",
"rustls 0.21.12",
"rustls 0.23.40",
"rustls 0.23.41",
"rustls-native-certs",
"rustls-pki-types",
"tokio",
@@ -893,7 +893,7 @@ version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"cexpr",
"clang-sys",
"itertools 0.13.0",
@@ -922,6 +922,12 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.13.0"
@@ -1060,9 +1066,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
[[package]]
name = "cc"
version = "1.2.64"
version = "1.2.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f"
checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -1104,9 +1110,9 @@ dependencies = [
[[package]]
name = "chacha20"
version = "0.10.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
@@ -1402,7 +1408,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "coyote-ai"
version = "0.7.0"
version = "0.7.3"
dependencies = [
"ansi_colours",
"anyhow",
@@ -1414,7 +1420,7 @@ dependencies = [
"aws-smithy-types",
"base64",
"bincode 2.0.1",
"bitflags",
"bitflags 2.13.0",
"bm25",
"bytes",
"chrono",
@@ -1556,7 +1562,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"crossterm_winapi",
"derive_more 2.1.1",
"document-features",
@@ -1727,6 +1733,38 @@ dependencies = [
"syn",
]
[[package]]
name = "defmt"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f"
dependencies = [
"bitflags 1.3.2",
"defmt-macros",
]
[[package]]
name = "defmt-macros"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b"
dependencies = [
"defmt-parser",
"proc-macro-error2",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "defmt-parser"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
dependencies = [
"thiserror 2.0.18",
]
[[package]]
name = "der"
version = "0.7.10"
@@ -1872,7 +1910,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"objc2",
]
@@ -2814,7 +2852,7 @@ dependencies = [
"http 1.4.2",
"hyper 1.10.1",
"hyper-util",
"rustls 0.23.40",
"rustls 0.23.41",
"rustls-native-certs",
"tokio",
"tokio-rustls 0.26.4",
@@ -3066,7 +3104,7 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"crossterm",
"dyn-clone",
"fuzzy-matcher",
@@ -3151,10 +3189,11 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jiff"
version = "0.2.28"
version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102"
checksum = "34f877a98676d2fb664698d74cc6a51ce6c484ce8c770f05d0108ec9090aeb46"
dependencies = [
"defmt",
"jiff-static",
"log",
"portable-atomic",
@@ -3164,9 +3203,9 @@ dependencies = [
[[package]]
name = "jiff-static"
version = "0.2.28"
version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47"
checksum = "0666b5ab5ecaca213fc2a85b8c0083d9004e84ee2d5f9a7e0017aaf50986f25f"
dependencies = [
"proc-macro2",
"quote",
@@ -3342,9 +3381,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.32"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
dependencies = [
"serde_core",
]
@@ -3518,7 +3557,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ecce9d566cb9234ae3db9e249c8b55665feaaf32b0859ff1e27e310d2beb3d8"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"combine",
"libc",
"mach2",
@@ -3570,7 +3609,7 @@ version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"cfg-if",
"cfg_aliases",
"libc",
@@ -3582,7 +3621,7 @@ version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"cfg-if",
"cfg_aliases",
"libc",
@@ -3684,7 +3723,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"objc2",
"objc2-core-graphics",
"objc2-foundation",
@@ -3696,7 +3735,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"objc2",
"objc2-foundation",
]
@@ -3717,7 +3756,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"dispatch2",
"objc2",
]
@@ -3728,7 +3767,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"dispatch2",
"objc2",
"objc2-core-foundation",
@@ -3761,7 +3800,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"objc2",
"objc2-core-foundation",
"objc2-core-graphics",
@@ -3779,7 +3818,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"block2",
"libc",
"objc2",
@@ -3802,7 +3841,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"objc2",
"objc2-core-foundation",
]
@@ -3813,7 +3852,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"objc2",
"objc2-core-foundation",
"objc2-foundation",
@@ -3825,7 +3864,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"block2",
"objc2",
"objc2-cloud-kit",
@@ -3877,7 +3916,7 @@ version = "6.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"libc",
"once_cell",
"onig_sys",
@@ -3916,7 +3955,7 @@ version = "0.10.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"cfg-if",
"foreign-types",
"libc",
@@ -4392,9 +4431,9 @@ dependencies = [
[[package]]
name = "quinn"
version = "0.11.9"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
dependencies = [
"bytes",
"cfg_aliases",
@@ -4402,7 +4441,7 @@ dependencies = [
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls 0.23.40",
"rustls 0.23.41",
"socket2 0.6.4",
"thiserror 2.0.18",
"tokio",
@@ -4412,9 +4451,9 @@ dependencies = [
[[package]]
name = "quinn-proto"
version = "0.11.14"
version = "0.11.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e"
dependencies = [
"aws-lc-rs",
"bytes",
@@ -4423,7 +4462,7 @@ dependencies = [
"rand 0.9.4",
"ring",
"rustc-hash",
"rustls 0.23.40",
"rustls 0.23.41",
"rustls-pki-types",
"slab",
"thiserror 2.0.18",
@@ -4448,9 +4487,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.45"
version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
dependencies = [
"proc-macro2",
]
@@ -4492,7 +4531,7 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [
"chacha20 0.10.0",
"chacha20 0.10.1",
"getrandom 0.4.3",
"rand_core 0.10.1",
]
@@ -4557,7 +4596,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
"bitflags 2.13.0",
]
[[package]]
@@ -4669,7 +4708,7 @@ dependencies = [
"native-tls",
"percent-encoding",
"pin-project-lite",
"rustls 0.23.40",
"rustls 0.23.41",
"rustls-native-certs",
"rustls-pki-types",
"serde",
@@ -4716,7 +4755,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls 0.23.40",
"rustls 0.23.41",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
@@ -4762,9 +4801,9 @@ dependencies = [
[[package]]
name = "rmcp"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0810a9f717d9828f475fe1f629f4c305c8464b7f496c3a854b58d29e65f4058e"
checksum = "1d1f571c72940a19d9532fe52dbea8bc9912bf1d766c2970bb824056b86f3f59"
dependencies = [
"async-trait",
"base64",
@@ -4789,9 +4828,9 @@ dependencies = [
[[package]]
name = "rmcp-macros"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6aefac48c364756e97f04c0401ba3231e8607882c7c1d92da0437dc16307904d"
checksum = "1aad0035b69380782d78ea95b508327e6deaa2235909053e596eea8f27b5e1d5"
dependencies = [
"darling 0.23.0",
"proc-macro2",
@@ -4898,7 +4937,7 @@ version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"errno",
"libc",
"linux-raw-sys",
@@ -4919,9 +4958,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.40"
version = "0.23.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
dependencies = [
"aws-lc-rs",
"log",
@@ -4966,7 +5005,7 @@ dependencies = [
"jni",
"log",
"once_cell",
"rustls 0.23.40",
"rustls 0.23.41",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki 0.103.13",
@@ -5129,7 +5168,7 @@ version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"core-foundation",
"core-foundation-sys",
"libc",
@@ -5152,7 +5191,7 @@ version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd568a4c9bb598e291a08244a5c1f5a8a6650bee243b5b0f8dbb3d9cc1d87fe8"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"cssparser",
"derive_more 0.99.20",
"fxhash",
@@ -5759,7 +5798,7 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"byteorder",
"enum-as-inner",
"libc",
@@ -6042,7 +6081,7 @@ version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls 0.23.40",
"rustls 0.23.41",
"tokio",
]
@@ -6165,7 +6204,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
dependencies = [
"async-compression",
"bitflags",
"bitflags 2.13.0",
"bytes",
"futures-core",
"futures-util",
@@ -6438,7 +6477,7 @@ dependencies = [
"flate2",
"log",
"percent-encoding",
"rustls 0.23.40",
"rustls 0.23.41",
"rustls-pki-types",
"serde",
"serde_json",
@@ -6704,7 +6743,7 @@ version = "0.31.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"rustix",
"wayland-backend",
"wayland-scanner",
@@ -6712,11 +6751,11 @@ dependencies = [
[[package]]
name = "wayland-protocols"
version = "0.32.12"
version = "0.32.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f"
checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"wayland-backend",
"wayland-client",
"wayland-scanner",
@@ -6728,7 +6767,7 @@ version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234"
dependencies = [
"bitflags",
"bitflags 2.13.0",
"wayland-backend",
"wayland-client",
"wayland-protocols",
@@ -7411,9 +7450,9 @@ dependencies = [
[[package]]
name = "zlib-rs"
version = "0.6.3"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513"
checksum = "977347db8caa080403f6b6b7c1cda9479a8e869316f7e13a59b19076a40f94e3"
[[package]]
name = "zmij"
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "coyote-ai"
version = "0.7.0"
version = "0.7.3"
edition = "2024"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "An all-in-one, batteries included LLM CLI Tool"
@@ -49,7 +49,7 @@ textwrap = "0.16.0"
ansi_colours = "1.2.2"
eventsource-stream = "0.2.3"
log = "0.4.28"
log4rs = { version = "1.4.0", features = ["file_appender"] }
log4rs = { version = "1.4.0", features = ["file_appender", "rolling_file_appender", "compound_policy", "fixed_window_roller", "size_trigger"] }
shell-words = "1.1.0"
sha2 = "0.10.8"
unicode-width = "0.2.0"
+2 -2
View File
@@ -98,7 +98,7 @@ You can use the following command to run a bash script that downloads and instal
OS (Linux/MacOS) and architecture (x86_64/arm64):
```shell
curl -fsSL https://raw.githubusercontent.com/Dark-Alex-17/coyote/main/install_coyote.sh | bash
curl -fsSL https://raw.githubusercontent.com/Dark-Alex-17/coyote/refs/heads/main/scripts/install_coyote.sh | bash
```
#### Windows/Linux/MacOS (`PowerShell`)
@@ -106,7 +106,7 @@ You can use the following command to run a PowerShell script that downloads and
for your OS (Windows/Linux/MacOS) and architecture (x86_64/arm64):
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -Command "iwr -useb https://raw.githubusercontent.com/Dark-Alex-17/coyote/main/scripts/install_coyote.ps1 | iex"
powershell -NoProfile -ExecutionPolicy Bypass -Command "iwr -useb https://raw.githubusercontent.com/Dark-Alex-17/coyote/refs/heads/main/scripts/install_coyote.ps1 | iex"
```
### Manual
+65 -59
View File
@@ -5,20 +5,19 @@
# sbx cp $HOME/.config/coyote/ testing:/home/agent/.config/
# sbx cp $HOME/.coyote_password testing:/home/agent/
# sbx run testing --kit ./sbx-kit/
schemaVersion: "1"
kind: agent
schemaVersion: '1'
kind: sandbox
name: coyote
displayName: Coyote
description: >
An all-in-one, batteries-included LLM CLI tool featuring Shell Assistant,
CLI & REPL mode, RAG, AI tools & agents, MCP servers, skills, and macros.
agent:
image: "docker/sandbox-templates:shell-docker"
sandbox:
image: 'docker/sandbox-templates:shell-docker'
aiFilename: COYOTE.md
# persistence: persistent
entrypoint:
run: ["bash", "-lc", "exec /home/agent/.cargo/bin/coyote"]
run: ['bash', '-lc', 'exec /home/agent/.cargo/bin/coyote']
network:
# Proxy-managed LLM providers: the proxy substitutes `proxy-managed` for
@@ -51,96 +50,96 @@ network:
serviceAuth:
openai:
headerName: Authorization
valueFormat: "Bearer %s"
valueFormat: 'Bearer %s'
anthropic:
headerName: x-api-key
valueFormat: "%s"
valueFormat: '%s'
gemini:
headerName: x-goog-api-key
valueFormat: "%s"
valueFormat: '%s'
cohere:
headerName: Authorization
valueFormat: "Bearer %s"
valueFormat: 'Bearer %s'
groq:
headerName: Authorization
valueFormat: "Bearer %s"
valueFormat: 'Bearer %s'
openrouter:
headerName: Authorization
valueFormat: "Bearer %s"
valueFormat: 'Bearer %s'
ai21:
headerName: Authorization
valueFormat: "Bearer %s"
valueFormat: 'Bearer %s'
cloudflare:
headerName: Authorization
valueFormat: "Bearer %s"
valueFormat: 'Bearer %s'
deepinfra:
headerName: Authorization
valueFormat: "Bearer %s"
valueFormat: 'Bearer %s'
deepseek:
headerName: Authorization
valueFormat: "Bearer %s"
valueFormat: 'Bearer %s'
mistral:
headerName: Authorization
valueFormat: "Bearer %s"
valueFormat: 'Bearer %s'
perplexity:
headerName: Authorization
valueFormat: "Bearer %s"
valueFormat: 'Bearer %s'
voyageai:
headerName: Authorization
valueFormat: "Bearer %s"
valueFormat: 'Bearer %s'
xai:
headerName: Authorization
valueFormat: "Bearer %s"
valueFormat: 'Bearer %s'
jina:
headerName: Authorization
valueFormat: "Bearer %s"
valueFormat: 'Bearer %s'
ernie:
headerName: Authorization
valueFormat: "Bearer %s"
valueFormat: 'Bearer %s'
hunyuan:
headerName: Authorization
valueFormat: "Bearer %s"
valueFormat: 'Bearer %s'
minimax:
headerName: Authorization
valueFormat: "Bearer %s"
valueFormat: 'Bearer %s'
moonshot:
headerName: Authorization
valueFormat: "Bearer %s"
valueFormat: 'Bearer %s'
qianwen:
headerName: Authorization
valueFormat: "Bearer %s"
valueFormat: 'Bearer %s'
zhipuai:
headerName: Authorization
valueFormat: "Bearer %s"
valueFormat: 'Bearer %s'
allowedDomains:
# Coyote release + self-update + model-registry sync
- "github.com:443"
- "api.github.com:443"
- "raw.githubusercontent.com:443"
- "objects.githubusercontent.com:443"
- "*.githubusercontent.com:443"
- 'github.com:443'
- 'api.github.com:443'
- 'raw.githubusercontent.com:443'
- 'objects.githubusercontent.com:443'
- '*.githubusercontent.com:443'
# Coyote install paths (cargo install + uv + rustup + Python tool deps at runtime)
- "crates.io:443"
- "static.crates.io:443"
- "pypi.org:443"
- "files.pythonhosted.org:443"
- "astral.sh:443"
- "sh.rustup.rs:443"
- "static.rust-lang.org:443"
- 'crates.io:443'
- 'static.crates.io:443'
- 'pypi.org:443'
- 'files.pythonhosted.org:443'
- 'astral.sh:443'
- 'sh.rustup.rs:443'
- 'static.rust-lang.org:443'
# LLM model OAuth + API endpoints
- "claude.ai:443"
- "console.anthropic.com:443"
- "accounts.google.com:443"
- 'claude.ai:443'
- 'console.anthropic.com:443'
- 'accounts.google.com:443'
# *.googleapis.com covers oauth2 + userinfo + VertexAI regional endpoints
# (*-aiplatform.googleapis.com). Do not narrow without re-checking VertexAI.
- "*.googleapis.com:443"
- '*.googleapis.com:443'
# Bedrock and GitHub Models use signed / GitHub-PAT auth that the proxy
# cannot rewrite. Domains are allow-listed; credentials must be injected
# separately (see README "Extending").
- "*.amazonaws.com:443"
- "models.inference.ai.azure.com:443"
- '*.amazonaws.com:443'
- 'models.inference.ai.azure.com:443'
credentials:
sources:
@@ -211,8 +210,9 @@ credentials:
environment:
variables:
IS_SANDBOX: "1"
IS_SANDBOX: '1'
COYOTE_LOG_LEVEL: INFO
COYOTE_CONFIG_DIR: /home/agent/.config/coyote
proxyManaged:
- OPENAI_API_KEY
- ANTHROPIC_API_KEY
@@ -250,25 +250,26 @@ commands:
libssl-dev \
pandoc \
bzip2
user: "1000"
user: '1000'
description: Install system prerequisites (including pandoc for fetch_url_via_curl)
- command: "curl -LsSf https://astral.sh/uv/install.sh | sh"
user: "1000"
- command: 'curl -LsSf https://astral.sh/uv/install.sh | sh'
user: '1000'
description: Install uv (required for Python-based custom tools)
- command: |
set -euo pipefail
USQL_VERSION="0.19.20"
USQL_VERSION=0.21.4
ARCH=$(uname -m)
case "$ARCH" in
x86_64) USQL_ARCH=amd64 ;;
aarch64) USQL_ARCH=arm64 ;;
*) echo "Unsupported arch for usql install: $ARCH" >&2; exit 1 ;;
esac
curl -sSL "https://github.com/xo/usql/releases/download/v${USQL_VERSION}/usql_static-${USQL_VERSION}-linux-${USQL_ARCH}.tar.bz2" -o /tmp/usql.tar.bz2
sudo tar -xjf /tmp/usql.tar.bz2 -C /usr/local/bin
sudo chmod +x /usr/local/bin/usql
rm -f /tmp/usql.tar.bz2
user: "1000"
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
curl -fsSL --retry 3 "https://github.com/xo/usql/releases/download/v${USQL_VERSION}/usql_static-${USQL_VERSION}-linux-${USQL_ARCH}.tar.bz2" -o "$TMPDIR/usql.tar.bz2"
tar -xjf "$TMPDIR/usql.tar.bz2" -C "$TMPDIR"
sudo install -m 0755 "$TMPDIR/usql_static" /usr/local/bin/usql
user: '1000'
description: Install the usql universal SQL CLI (used by the built-in sql agent and execute_sql_code tool)
- command: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
@@ -278,16 +279,21 @@ commands:
--target x86_64-unknown-linux-musl
. "$HOME/.cargo/env"
cargo install --locked coyote-ai
user: "1000"
user: '1000'
description: Install Coyote AI CLI via Rust's Cargo
startup:
- command: ["sh", "-c", "test -f \"$HOME/.config/coyote/config.yaml\" || coyote --info >/dev/null 2>&1 || true"]
user: "1000"
- command:
[
'sh',
'-c',
'test -f "$HOME/.config/coyote/config.yaml" || coyote --info >/dev/null 2>&1 || true',
]
user: '1000'
background: false
description: Bootstrap Coyote config directory on first sandbox start
memory: |
agentContext: |
## Sandbox environment
You are running inside a Docker sandbox launched via `sbx run coyote`. The
+3 -3
View File
@@ -39,7 +39,7 @@ switch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) {
if (-not $BinDir) {
if ($isWin) { $BinDir = Join-Path $env:LOCALAPPDATA 'coyote\bin' }
else { $home = $env:HOME; if (-not $home) { $home = (Get-Item -Path ~).FullName }; $BinDir = Join-Path $home '.local/bin' }
else { $userHome = $env:HOME; if (-not $userHome) { $userHome = (Get-Item -Path ~).FullName }; $BinDir = Join-Path $userHome '.local/bin' }
}
New-Item -ItemType Directory -Force -Path $BinDir | Out-Null
@@ -95,13 +95,13 @@ if ($asset.name -match '\.zip$') {
[System.IO.Compression.ZipFile]::ExtractToDirectory($archive, $extractDir)
} elseif ($asset.name -match '\.tar\.gz$' -or $asset.name -match '\.tgz$') {
$tar = Get-Command tar -ErrorAction SilentlyContinue
if ($tar) { & $tar.FullName -xzf $archive -C $extractDir }
if ($tar) { & $tar.Source -xzf $archive -C $extractDir }
else { Fail "Asset is tar archive but 'tar' is not available." }
} else {
try { Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory($archive, $extractDir) }
catch {
$tar = Get-Command tar -ErrorAction SilentlyContinue
if ($tar) { & $tar.FullName -xf $archive -C $extractDir } else { Fail "Unknown archive format; neither zip nor tar workable." }
if ($tar) { & $tar.Source -xf $archive -C $extractDir } else { Fail "Unknown archive format; neither zip nor tar workable." }
}
}
Regular → Executable
+13 -22
View File
@@ -133,30 +133,21 @@ else
echo "Error: unsupported OS for this installer: $OS" >&2; exit 1
fi
DL_URLS=$(grep -oE '"browser_download_url":[[:space:]]*"[^"]+"' "$JSON" \
| sed -E 's/.*"browser_download_url":[[:space:]]*"//; s/"$//' \
|| true)
ASSET_NAME=""; ASSET_URL=""
for candidate in "${ASSET_CANDIDATES[@]}"; do
NAME=$(grep -oE '"name":\s*"[^"]+"' "$JSON" | sed 's/"name":\s*"//; s/"$//' | grep -Fx "$candidate" || true)
if [[ -n "$NAME" ]]; then
ASSET_NAME="$NAME"
ASSET_URL=$(awk -v pat="$NAME" '
BEGIN{ FS=":"; want=0 }
/"name"/ {
line=$0;
gsub(/^\s+|\s+$/,"",line);
gsub(/"name"\s*:\s*"|"/ ,"", line);
want = (line==pat) ? 1 : 0;
next
}
want==1 && /"browser_download_url"/ {
u=$0;
gsub(/^\s+|\s+$/,"",u);
gsub(/.*"browser_download_url"\s*:\s*"|".*/ ,"", u);
print u;
exit
}
' "$JSON")
if [[ -n "$ASSET_URL" ]]; then break; fi
fi
while IFS= read -r url; do
[[ -z "$url" ]] && continue
if [[ "$url" == */"$candidate" ]]; then
ASSET_NAME="$candidate"
ASSET_URL="$url"
break
fi
done <<< "$DL_URLS"
[[ -n "$ASSET_URL" ]] && break
done
if [[ -z "$ASSET_URL" ]]; then
+1
View File
@@ -147,6 +147,7 @@ const SBX_KIT_DIR_NAME: &str = "sbx-kit";
const SBX_KIT_HASH_FILE: &str = "kit.sha256";
const SBX_MIXIN_FILE_NAME: &str = "sbx-mixin.yaml";
const SBX_VAULT_MIXINS_DIR_NAME: &str = "sbx-vault-mixins";
const SBX_MIXIN_KITS_DIR_NAME: &str = "sbx-mixin-kits";
const GIT_DIR_NAME: &str = ".git";
const GITIGNORE_FILE_NAME: &str = ".gitignore";
const DEFAULT_VISIBLE_TOOLS: [&str; 18] = [
+14 -4
View File
@@ -4,8 +4,8 @@ use super::{
ENV_FILE_NAME, FUNCTIONS_BIN_DIR_NAME, FUNCTIONS_DIR_NAME, GLOBAL_TOOLS_DIR_NAME,
GLOBAL_TOOLS_UTILS_DIR_NAME, MACROS_DIR_NAME, MCP_FILE_NAME, MEMORY_DIR_NAME,
MEMORY_INDEX_FILE_NAME, ModelsOverride, RAGS_DIR_NAME, ROLES_DIR_NAME, SBX_KIT_DIR_NAME,
SBX_KIT_HASH_FILE, SBX_MIXIN_FILE_NAME, SBX_VAULT_MIXINS_DIR_NAME, SKILLS_DIR_NAME,
WORKSPACE_MEMORY_DIR_NAME,
SBX_KIT_HASH_FILE, SBX_MIXIN_FILE_NAME, SBX_MIXIN_KITS_DIR_NAME, SBX_VAULT_MIXINS_DIR_NAME,
SKILLS_DIR_NAME, WORKSPACE_MEMORY_DIR_NAME,
};
use crate::client::ProviderModels;
use crate::utils::{get_env_name, list_file_names, normalize_env_name};
@@ -33,8 +33,14 @@ pub fn local_path(name: &str) -> PathBuf {
}
pub fn cache_path() -> PathBuf {
let base_dir = dirs::cache_dir().unwrap_or_else(env::temp_dir);
base_dir.join(env!("CARGO_CRATE_NAME"))
if let Ok(v) = env::var(get_env_name("cache_dir")) {
PathBuf::from(v)
} else if let Ok(v) = env::var("XDG_CACHE_HOME") {
PathBuf::from(v).join(env!("CARGO_CRATE_NAME"))
} else {
let base_dir = dirs::cache_dir().unwrap_or_else(env::temp_dir);
base_dir.join(env!("CARGO_CRATE_NAME"))
}
}
pub fn sandbox_kit_override() -> Option<PathBuf> {
@@ -148,6 +154,10 @@ pub fn sbx_vault_mixins_hash_file() -> PathBuf {
sbx_vault_mixins_dir().join(SBX_KIT_HASH_FILE)
}
pub fn sbx_mixin_kits_dir() -> PathBuf {
cache_path().join(SBX_MIXIN_KITS_DIR_NAME)
}
pub fn config_file() -> PathBuf {
match env::var(get_env_name("config_file")) {
Ok(value) => PathBuf::from(value),
+23 -4
View File
@@ -39,7 +39,10 @@ use client::ClientConfig;
use inquire::{Select, Text, set_global_render_config};
use log::{LevelFilter, warn};
use log4rs::append::console::ConsoleAppender;
use log4rs::append::file::FileAppender;
use log4rs::append::rolling_file::RollingFileAppender;
use log4rs::append::rolling_file::policy::compound::CompoundPolicy;
use log4rs::append::rolling_file::policy::compound::roll::fixed_window::FixedWindowRoller;
use log4rs::append::rolling_file::policy::compound::trigger::size::SizeTrigger;
use log4rs::config::{Appender, Logger, Root};
use log4rs::encode::pattern::PatternEncoder;
use oauth::OAuthProvider;
@@ -137,7 +140,10 @@ async fn main() -> Result<()> {
)
.await?,
);
let ctx = RequestContext::bootstrap(app_state, working_mode, info_flag)?;
let mut ctx = RequestContext::bootstrap(app_state, working_mode, info_flag)?;
let app_config = Arc::clone(&ctx.app.config);
ctx.bootstrap_tools(&app_config, start_mcp_servers, abort_signal.clone())
.await?;
{
let app = &*ctx.app.config;
@@ -582,7 +588,20 @@ fn setup_logger() -> Result<Option<PathBuf>> {
}
Some(path) => {
ensure_parent_exists(&path)?;
let file_appender = FileAppender::builder().encoder(encoder.clone()).build(path);
let archive_pattern = path
.with_extension("archived.{}.log")
.to_string_lossy()
.into_owned();
let trigger = SizeTrigger::new(10 * 1024 * 1024);
let roller = FixedWindowRoller::builder()
.build(&archive_pattern, 5)
.unwrap();
let policy = CompoundPolicy::new(Box::new(trigger), Box::new(roller));
let file_appender = RollingFileAppender::builder()
.encoder(encoder.clone())
.build(path, Box::new(policy));
match file_appender {
Ok(appender) => {
@@ -605,7 +624,7 @@ fn setup_logger() -> Result<Option<PathBuf>> {
fn init_file_logger(
log_level: LevelFilter,
log_filter: Option<String>,
file_appender: FileAppender,
file_appender: RollingFileAppender,
) -> log4rs::Config {
let root_log_level = if log_filter.is_some() {
LevelFilter::Off
+205
View File
@@ -1,13 +1,16 @@
use std::env;
use std::fs;
use std::fs::{read_dir, read_to_string};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde_yaml::Value;
use sha2::{Digest, Sha256};
use crate::config::paths;
const SBX_MIXIN_FILE_NAME: &str = "sbx-mixin.yaml";
const KIT_SPEC_FILE_NAME: &str = "spec.yaml";
#[derive(Debug, Clone)]
pub struct DiscoveredMixin {
@@ -17,6 +20,46 @@ pub struct DiscoveredMixin {
pub domain_count: usize,
}
impl DiscoveredMixin {
pub fn kit_path(&self) -> Result<PathBuf> {
if self.path.is_dir() {
return Ok(self.path.clone());
}
wrap_mixin_as_kit(&self.path)
}
}
pub fn wrap_mixin_as_kit(mixin_path: &Path) -> Result<PathBuf> {
let bytes = fs::read(mixin_path)
.with_context(|| format!("Failed to read sbx mixin {}", mixin_path.display()))?;
let mut hasher = Sha256::new();
hasher.update(&bytes);
let hash = format!("{:x}", hasher.finalize());
let kit_dir = paths::sbx_mixin_kits_dir().join(&hash);
let spec_path = kit_dir.join(KIT_SPEC_FILE_NAME);
if let Ok(existing) = fs::read(&spec_path)
&& existing == bytes
{
return Ok(kit_dir);
}
fs::create_dir_all(&kit_dir)
.with_context(|| format!("Failed to create mixin kit dir {}", kit_dir.display()))?;
fs::write(&spec_path, &bytes)
.with_context(|| format!("Failed to write {}", spec_path.display()))?;
debug!(
"Wrapped mixin {} as kit at {}",
mixin_path.display(),
kit_dir.display()
);
Ok(kit_dir)
}
pub fn discover() -> Result<Vec<DiscoveredMixin>> {
let mut out = Vec::new();
@@ -234,4 +277,166 @@ network:
let found = collect_subdir_mixins(&absent);
assert!(found.is_empty());
}
mod wrap_as_kit {
use super::*;
use serial_test::serial;
use std::ffi::OsString;
struct TestCacheDirGuard {
key: String,
previous: Option<OsString>,
path: PathBuf,
}
impl TestCacheDirGuard {
fn new() -> Self {
let key = crate::utils::get_env_name("cache_dir");
let previous = env::var_os(&key);
let nanos = time::SystemTime::now()
.duration_since(time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = env::temp_dir().join(format!("coyote-mixin-wrap-cache-{nanos}"));
fs::create_dir_all(&path).unwrap();
unsafe {
env::set_var(&key, &path);
}
Self {
key,
previous,
path,
}
}
}
impl Drop for TestCacheDirGuard {
fn drop(&mut self) {
unsafe {
match &self.previous {
Some(v) => env::set_var(&self.key, v),
None => env::remove_var(&self.key),
}
}
let _ = fs::remove_dir_all(&self.path);
}
}
fn write_mixin(name: &str, content: &str) -> PathBuf {
let root = unique_root(&format!("wrap-src-{name}"));
let path = root.join("sbx-mixin.yaml");
fs::write(&path, content).unwrap();
path
}
#[test]
#[serial]
fn wrap_mixin_as_kit_creates_spec_yaml_with_original_content() {
let _guard = TestCacheDirGuard::new();
let content = "schemaVersion: \"1\"\nkind: mixin\nname: probe\n";
let mixin = write_mixin("content", content);
let kit_dir = wrap_mixin_as_kit(&mixin).unwrap();
let spec = kit_dir.join("spec.yaml");
assert!(spec.exists(), "spec.yaml must exist in wrapped kit dir");
assert_eq!(fs::read_to_string(&spec).unwrap(), content);
}
#[test]
#[serial]
fn wrap_mixin_as_kit_is_deterministic_for_identical_content() {
let _guard = TestCacheDirGuard::new();
let content = "schemaVersion: \"1\"\nkind: mixin\nname: probe\n";
let mixin_one = write_mixin("dedup-1", content);
let mixin_two = write_mixin("dedup-2", content);
let kit_a = wrap_mixin_as_kit(&mixin_one).unwrap();
let kit_b = wrap_mixin_as_kit(&mixin_two).unwrap();
assert_eq!(
kit_a, kit_b,
"same content should share the same content-addressed kit dir"
);
}
#[test]
#[serial]
fn wrap_mixin_as_kit_different_content_yields_different_dirs() {
let _guard = TestCacheDirGuard::new();
let mixin_a = write_mixin("diff-a", "kind: mixin\nname: a\n");
let mixin_b = write_mixin("diff-b", "kind: mixin\nname: b\n");
let kit_a = wrap_mixin_as_kit(&mixin_a).unwrap();
let kit_b = wrap_mixin_as_kit(&mixin_b).unwrap();
assert_ne!(
kit_a, kit_b,
"different content must hash to different kit dirs"
);
}
#[test]
#[serial]
fn wrap_mixin_as_kit_is_idempotent_on_cache_hit() {
let _guard = TestCacheDirGuard::new();
let mixin = write_mixin("idempotent", "kind: mixin\nname: probe\n");
let kit_first = wrap_mixin_as_kit(&mixin).unwrap();
let spec = kit_first.join("spec.yaml");
let mtime_first = fs::metadata(&spec).unwrap().modified().unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
let kit_second = wrap_mixin_as_kit(&mixin).unwrap();
let mtime_second = fs::metadata(kit_second.join("spec.yaml"))
.unwrap()
.modified()
.unwrap();
assert_eq!(kit_first, kit_second);
assert_eq!(
mtime_first, mtime_second,
"cache hit must not rewrite spec.yaml"
);
}
#[test]
#[serial]
fn kit_path_passes_through_existing_directory() {
let _guard = TestCacheDirGuard::new();
let dir = unique_root("kit-path-dir-passthrough");
let m = DiscoveredMixin {
path: dir.clone(),
label: "vault".into(),
install_count: 1,
domain_count: 1,
};
assert_eq!(m.kit_path().unwrap(), dir);
}
#[test]
#[serial]
fn kit_path_wraps_file_into_kit_dir() {
let _guard = TestCacheDirGuard::new();
let mixin = write_mixin("kit-path-wrap", "kind: mixin\nname: probe\n");
let m = DiscoveredMixin {
path: mixin.clone(),
label: mixin.display().to_string(),
install_count: 0,
domain_count: 0,
};
let wrapped = m.kit_path().unwrap();
assert!(wrapped.is_dir(), "kit_path of a file should be a directory");
assert!(wrapped.join("spec.yaml").exists());
assert_ne!(
wrapped, mixin,
"kit_path should not return the original file path"
);
}
}
}
+111 -24
View File
@@ -69,7 +69,7 @@ pub fn launch(name: Option<String>, fresh: bool, no_mixins: bool) -> Result<()>
}
}
exec_run(&name, &kit_path)
exec_run(&name)
}
fn ensure_sbx_installed() -> Result<()> {
@@ -316,6 +316,7 @@ fn sandbox_exists(name: &str) -> Result<bool> {
fn create_sandbox(name: &str, kit_path: &Path, mixins: &[DiscoveredMixin]) -> Result<()> {
info!("Creating sandbox '{name}'");
let args = build_create_args(name, kit_path, mixins)?;
debug!("sbx {}", args.join(" "));
let status = Command::new(SBX_BINARY)
.args(&args)
.stdin(Stdio::inherit())
@@ -342,22 +343,23 @@ fn build_create_args(
let mut args = vec![
"create".to_string(),
"--name".to_string(),
name.to_string(),
"--kit".to_string(),
kit_str.to_string(),
];
for mixin in mixins {
let mixin_str = mixin
.path
let mixin_kit = mixin.kit_path()?;
let mixin_str = mixin_kit
.to_str()
.ok_or_else(|| anyhow!("Mixin path is not valid UTF-8: {}", mixin.path.display()))?;
.ok_or_else(|| anyhow!("Mixin kit path is not valid UTF-8: {}", mixin_kit.display()))?
.to_string();
args.push("--kit".to_string());
args.push(mixin_str.to_string());
args.push(mixin_str);
}
args.push(SANDBOX_AGENT.to_string());
args.push("--name".to_string());
args.push(name.to_string());
args.push(".".to_string());
Ok(args)
@@ -368,10 +370,17 @@ fn copy_host_files(name: &str) -> Result<()> {
let home_dir = dirs::home_dir().context("Could not determine home directory")?;
if config_dir.exists() {
ensure_sandbox_dir(name, "/home/agent/.config")?;
let src = format!("{}/", config_dir.display());
let dest = format!("{name}:/home/agent/.config/");
sbx_cp(&src, &dest)?;
let sandbox_config_dir = "/home/agent/.config/coyote";
ensure_sandbox_dir(name, sandbox_config_dir)?;
let dest = format!("{name}:{sandbox_config_dir}/");
for entry in fs::read_dir(&config_dir)
.with_context(|| format!("Failed to read {}", config_dir.display()))?
{
let entry = entry?;
let path = entry.path();
sbx_cp(&path.display().to_string(), &dest)?;
}
chown_agent_recursive(name, sandbox_config_dir)?;
} else {
debug!(
"Skipping config copy: {} does not exist",
@@ -389,6 +398,7 @@ fn copy_host_files(name: &str) -> Result<()> {
}
let dest = format!("{name}:{dest_path}");
sbx_cp(&password_file.display().to_string(), &dest)?;
chown_agent_recursive(name, &dest_path)?;
}
Some(password_file) => {
debug!(
@@ -500,12 +510,10 @@ fn sbx_cp(src: &str, dest: &str) -> Result<()> {
Ok(())
}
fn exec_run(name: &str, kit_path: &Path) -> Result<()> {
let kit_str = kit_path
.to_str()
.ok_or_else(|| anyhow!("Kit path is not valid UTF-8: {}", kit_path.display()))?;
fn exec_run(name: &str) -> Result<()> {
debug!("sbx run --name {name}");
let status = Command::new(SBX_BINARY)
.args(["run", name, "--kit", kit_str])
.args(["run", "--name", name])
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
@@ -519,6 +527,27 @@ fn exec_run(name: &str, kit_path: &Path) -> Result<()> {
Ok(())
}
fn chown_agent_recursive(sandbox: &str, path: &str) -> Result<()> {
let path_q = shell_words::quote(path);
let cmd = format!("sudo chown -R agent:agent {path_q}");
debug!("sbx exec {sandbox}: {cmd}");
let status = Command::new(SBX_BINARY)
.args(["exec", sandbox, "sh", "-c", &cmd])
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.context("Failed to spawn `sbx exec` to chown copied files")?;
if !status.success() {
bail!("Chowning '{path}' in sandbox failed: sbx exec exited with {status}");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -590,15 +619,24 @@ mod tests {
#[test]
fn build_create_args_emits_base_kit_before_mixins() {
let kit = PathBuf::from("/cache/sbx-kit");
let unique = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir_a = env::temp_dir().join(format!("coyote-mixin-a-{unique}"));
let dir_b = env::temp_dir().join(format!("coyote-mixin-b-{unique}"));
fs::create_dir_all(&dir_a).unwrap();
fs::create_dir_all(&dir_b).unwrap();
let mixins = vec![
DiscoveredMixin {
path: PathBuf::from("/cfg/sbx-mixin.yaml"),
path: dir_a.clone(),
label: "user".into(),
install_count: 0,
domain_count: 0,
},
DiscoveredMixin {
path: PathBuf::from("/cfg/agents/sql/sbx-mixin.yaml"),
path: dir_b.clone(),
label: "sql".into(),
install_count: 0,
domain_count: 0,
@@ -611,18 +649,21 @@ mod tests {
args,
vec![
"create".to_string(),
"--name".to_string(),
"my-box".to_string(),
"--kit".to_string(),
"/cache/sbx-kit".to_string(),
"--kit".to_string(),
"/cfg/sbx-mixin.yaml".to_string(),
dir_a.display().to_string(),
"--kit".to_string(),
"/cfg/agents/sql/sbx-mixin.yaml".to_string(),
dir_b.display().to_string(),
"coyote".to_string(),
"--name".to_string(),
"my-box".to_string(),
".".to_string(),
]
);
let _ = fs::remove_dir_all(&dir_a);
let _ = fs::remove_dir_all(&dir_b);
}
#[test]
@@ -633,11 +674,11 @@ mod tests {
args,
vec![
"create".to_string(),
"--name".to_string(),
"box".to_string(),
"--kit".to_string(),
"/cache/sbx-kit".to_string(),
"coyote".to_string(),
"--name".to_string(),
"box".to_string(),
".".to_string(),
]
);
@@ -645,6 +686,7 @@ mod tests {
mod vault_mixins {
use super::*;
use crate::utils::get_env_name;
use gman::providers::aws_secrets_manager::AwsSecretsManagerProvider;
use gman::providers::azure_key_vault::AzureKeyVaultProvider;
use gman::providers::gcp_secret_manager::GcpSecretManagerProvider;
@@ -652,6 +694,46 @@ mod tests {
use gman::providers::local::LocalProvider;
use gman::providers::one_password::OnePasswordProvider;
use serial_test::serial;
use std::time::{SystemTime, UNIX_EPOCH};
struct TestCacheDirGuard {
key: String,
previous: Option<std::ffi::OsString>,
path: PathBuf,
}
impl TestCacheDirGuard {
fn new() -> Self {
let key = get_env_name("cache_dir");
let previous = env::var_os(&key);
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = env::temp_dir().join(format!("coyote-sandbox-vault-tests-{unique}"));
fs::create_dir_all(&path).unwrap();
unsafe {
env::set_var(&key, &path);
}
Self {
key,
previous,
path,
}
}
}
impl Drop for TestCacheDirGuard {
fn drop(&mut self) {
unsafe {
match &self.previous {
Some(v) => env::set_var(&self.key, v),
None => env::remove_var(&self.key),
}
}
let _ = fs::remove_dir_all(&self.path);
}
}
#[test]
fn returns_none_for_local() {
@@ -664,6 +746,7 @@ mod tests {
#[test]
#[serial]
fn returns_some_for_aws() {
let _guard = TestCacheDirGuard::new();
let p = SupportedProvider::AwsSecretsManager {
provider_def: AwsSecretsManagerProvider {
aws_profile: None,
@@ -680,6 +763,7 @@ mod tests {
#[test]
#[serial]
fn returns_some_for_gcp() {
let _guard = TestCacheDirGuard::new();
let p = SupportedProvider::GcpSecretManager {
provider_def: GcpSecretManagerProvider {
gcp_project_id: None,
@@ -695,6 +779,7 @@ mod tests {
#[test]
#[serial]
fn returns_some_for_one_password() {
let _guard = TestCacheDirGuard::new();
let p = SupportedProvider::OnePassword {
provider_def: OnePasswordProvider {
vault: None,
@@ -711,6 +796,7 @@ mod tests {
#[test]
#[serial]
fn returns_some_for_azure() {
let _guard = TestCacheDirGuard::new();
let p = SupportedProvider::AzureKeyVault {
provider_def: AzureKeyVaultProvider { vault_name: None },
};
@@ -724,6 +810,7 @@ mod tests {
#[test]
#[serial]
fn returns_some_for_gopass() {
let _guard = TestCacheDirGuard::new();
let p = SupportedProvider::Gopass {
provider_def: GopassProvider { store: None },
};
+33 -8
View File
@@ -1,9 +1,11 @@
use crate::config::paths;
use colored::Colorize;
use fancy_regex::Regex;
use std::fs::File;
use std::fs::{self, File};
use std::io::{BufRead, BufReader, Seek, SeekFrom};
use std::process;
use std::time::Duration;
use tokio::time::sleep;
pub async fn tail_logs(no_color: bool) {
let re = Regex::new(r"^(?P<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s+<(?P<opid>[^\s>]+)>\s+\[(?P<level>[A-Z]+)\]\s+(?P<logger>[^:]+):(?P<line>\d+)\s+-\s+(?P<message>.*)$").unwrap();
@@ -16,20 +18,43 @@ pub async fn tail_logs(no_color: bool) {
process::exit(1);
};
let mut lines = reader.lines();
let mut line_buf = String::new();
loop {
if let Some(Ok(line)) = lines.next() {
if no_color {
println!("{line}");
} else {
let colored_line = colorize_log_line(&line, &re);
println!("{colored_line}");
match reader.read_line(&mut line_buf) {
Ok(0) => {
if file_was_rotated(&file_path, &mut reader) {
let file = File::open(&file_path).expect("Cannot open file");
reader = BufReader::new(file);
}
sleep(Duration::from_millis(100)).await;
}
Ok(_) => {
let line = line_buf.trim_end();
if no_color {
println!("{line}");
} else {
let colored_line = colorize_log_line(line, &re);
println!("{colored_line}");
}
line_buf.clear();
}
Err(_) => {
line_buf.clear();
sleep(Duration::from_millis(100)).await;
}
}
}
}
fn file_was_rotated(path: &std::path::Path, reader: &mut BufReader<File>) -> bool {
let current_pos = reader.stream_position().unwrap_or(0);
match fs::metadata(path) {
Ok(metadata) => metadata.len() < current_pos,
Err(_) => true,
}
}
fn colorize_log_line(line: &str, re: &Regex) -> String {
if let Some(caps) = re.captures(line).expect("Failed to capture log line") {
let level = &caps["level"];