mirror of
https://github.com/khodges42/exoshell.git
synced 2026-06-14 18:08:37 +00:00
Updates for command shell
This commit is contained in:
parent
40216fb635
commit
ba19b6bcff
460
Cargo.lock
generated
460
Cargo.lock
generated
|
|
@ -66,30 +66,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
|
|
@ -102,15 +82,6 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
|
|
@ -154,33 +125,12 @@ version = "0.1.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
|
|
@ -257,8 +207,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi 5.3.0",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -269,30 +235,11 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
|||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"r-efi 6.0.0",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http",
|
||||
"indexmap",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
|
|
@ -363,7 +310,6 @@ dependencies = [
|
|||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
|
|
@ -387,22 +333,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -423,11 +354,9 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -605,18 +534,18 @@ version = "0.4.30"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.1"
|
||||
|
|
@ -628,72 +557,12 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.116"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
|
|
@ -706,12 +575,6 @@ version = "0.2.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.5"
|
||||
|
|
@ -721,6 +584,15 @@ dependencies = [
|
|||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
|
|
@ -740,6 +612,61 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.4",
|
||||
"lru-slab",
|
||||
"rand",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
|
|
@ -749,12 +676,47 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.28"
|
||||
|
|
@ -763,30 +725,27 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
|||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
|
|
@ -796,6 +755,7 @@ dependencies = [
|
|||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -812,6 +772,12 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
|
|
@ -832,6 +798,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
|
|
@ -844,6 +811,7 @@ version = "1.14.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
|
|
@ -870,38 +838,6 @@ version = "1.0.23"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"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 = "semver"
|
||||
version = "1.0.28"
|
||||
|
|
@ -1053,27 +989,6 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
|
|
@ -1117,6 +1032,21 @@ dependencies = [
|
|||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.3"
|
||||
|
|
@ -1144,16 +1074,6 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
|
|
@ -1322,12 +1242,6 @@ version = "1.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
|
|
@ -1473,41 +1387,31 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
|
|
@ -1725,6 +1629,26 @@ dependencies = [
|
|||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.8"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ license = "GPL-3.0-or-later"
|
|||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
futures-util = "0.3"
|
||||
reqwest = { version = "0.12", features = ["json", "stream"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
thiserror = "2"
|
||||
|
|
|
|||
|
|
@ -89,7 +89,21 @@ Available actions:
|
|||
|
||||
## Risk Detection
|
||||
|
||||
Phase 2 includes a simple non-blocking detector for obvious risky commands. It flags patterns such as recursive forced deletion, disk formatting, recursive permission changes, downloaded content piped to an interpreter, credential exposure, and package removal.
|
||||
Phase 2 includes a simple non-blocking detector for obvious risky commands. The detector is policy-driven: Exoshell ships with conservative defaults, and users can add or replace rules at runtime through config.
|
||||
|
||||
Default rules flag patterns such as recursive forced deletion, disk formatting, recursive permission changes, downloaded content piped to an interpreter, credential exposure, and package removal.
|
||||
|
||||
Example custom rule:
|
||||
|
||||
```toml
|
||||
[commands.risk]
|
||||
include_defaults = true
|
||||
|
||||
[[commands.risk.rules]]
|
||||
match_all = ["kubectl delete", "--all"]
|
||||
reason = "cluster-wide deletion"
|
||||
shell = "posix"
|
||||
```
|
||||
|
||||
False positives are acceptable. A warning means "review this carefully," not "this command is definitely harmful." Lack of a warning does not mean a command is safe.
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,34 @@ stance = "operator"
|
|||
|
||||
Local provider URLs such as `localhost` and `127.0.0.1` do not require an API key.
|
||||
|
||||
## Configure Command Risk Rules
|
||||
|
||||
Exoshell ships with conservative default rules for obvious risky command suggestions. You can add your own rules at runtime:
|
||||
|
||||
```toml
|
||||
[commands.risk]
|
||||
include_defaults = true
|
||||
|
||||
[[commands.risk.rules]]
|
||||
match_all = ["kubectl delete", "--all"]
|
||||
reason = "cluster-wide deletion"
|
||||
shell = "posix"
|
||||
|
||||
[[commands.risk.rules]]
|
||||
match_all = ["terraform apply"]
|
||||
reason = "infrastructure mutation"
|
||||
shell = "posix"
|
||||
```
|
||||
|
||||
`match_all` is a list of case-insensitive substrings that must all appear in the suggested command. `shell` is optional; use `posix` or `powershell`.
|
||||
|
||||
To replace the built-in defaults entirely:
|
||||
|
||||
```toml
|
||||
[commands.risk]
|
||||
include_defaults = false
|
||||
```
|
||||
|
||||
## Start Exoshell
|
||||
|
||||
Run with defaults:
|
||||
|
|
|
|||
17
src/app.rs
17
src/app.rs
|
|
@ -1,13 +1,14 @@
|
|||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::commands::{CommandSuggestion, parse_command_suggestions};
|
||||
use crate::commands::{CommandSuggestion, parse_command_suggestions_with_policy};
|
||||
use crate::config::Config;
|
||||
use crate::context::{
|
||||
ContextError, ContextPriority, ContextProviderRegistry, ContextProviderRequest,
|
||||
SessionContextStore, budget_warning, prune_context, register_default_context_providers,
|
||||
render_context_details, render_context_list, render_context_stats,
|
||||
};
|
||||
use crate::formatting::render_assistant_output_with_policy;
|
||||
use crate::prompts::{Stance, assemble_prompt, render_prompt_estimate};
|
||||
use crate::providers::{ChatMessage, ChatRequest, ChatResponse, ChatRole, Provider, ProviderError};
|
||||
use crate::repl::ReplError;
|
||||
|
|
@ -78,7 +79,8 @@ impl App {
|
|||
self.conversation
|
||||
.push(ChatMessage::new(ChatRole::Assistant, response.clone()));
|
||||
self.transcript.record_assistant(&response);
|
||||
self.last_command_suggestions = parse_command_suggestions(&response);
|
||||
self.last_command_suggestions =
|
||||
parse_command_suggestions_with_policy(&response, &self.config.commands.risk);
|
||||
for suggestion in &self.last_command_suggestions {
|
||||
self.transcript.record_command_suggestion(suggestion);
|
||||
}
|
||||
|
|
@ -277,6 +279,10 @@ impl App {
|
|||
Ok(Some(path))
|
||||
}
|
||||
|
||||
pub fn render_assistant_output(&self, response: &str) -> String {
|
||||
render_assistant_output_with_policy(response, &self.config.commands.risk)
|
||||
}
|
||||
|
||||
fn assembled_messages(&mut self) -> Result<Vec<ChatMessage>, AppError> {
|
||||
let size = self.context_store.total_size();
|
||||
let budget = self.config.context.budget();
|
||||
|
|
@ -627,7 +633,9 @@ impl CliOptions {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::{InteractionConfig, ProviderConfig, ShellConfig, TranscriptConfig};
|
||||
use crate::config::{
|
||||
CommandConfig, InteractionConfig, ProviderConfig, ShellConfig, TranscriptConfig,
|
||||
};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[test]
|
||||
|
|
@ -959,6 +967,9 @@ mod tests {
|
|||
interaction: InteractionConfig {
|
||||
stance: Stance::Operator,
|
||||
},
|
||||
commands: CommandConfig {
|
||||
risk: crate::commands::CommandRiskPolicy::default(),
|
||||
},
|
||||
transcript: TranscriptConfig {
|
||||
directory: PathBuf::from("transcripts"),
|
||||
enabled: false,
|
||||
|
|
|
|||
349
src/commands.rs
349
src/commands.rs
|
|
@ -11,7 +11,8 @@ pub struct CommandSuggestion {
|
|||
pub discarded: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommandShell {
|
||||
PowerShell,
|
||||
Posix,
|
||||
|
|
@ -28,6 +29,25 @@ impl fmt::Display for CommandShell {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||
pub enum CommandShellError {
|
||||
#[error("unknown command shell '{0}', expected powershell, posix, or unknown")]
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl std::str::FromStr for CommandShell {
|
||||
type Err = CommandShellError;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"powershell" | "pwsh" => Ok(Self::PowerShell),
|
||||
"posix" | "sh" | "bash" | "zsh" => Ok(Self::Posix),
|
||||
"unknown" => Ok(Self::Unknown),
|
||||
other => Err(CommandShellError::Unknown(other.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RiskLevel {
|
||||
Low,
|
||||
|
|
@ -51,14 +71,82 @@ pub struct CommandRisk {
|
|||
pub reasons: Vec<String>,
|
||||
}
|
||||
|
||||
impl CommandRisk {
|
||||
pub fn none() -> Self {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CommandRiskPolicy {
|
||||
pub include_defaults: bool,
|
||||
pub rules: Vec<CommandRiskRule>,
|
||||
}
|
||||
|
||||
impl CommandRiskPolicy {
|
||||
pub fn evaluate(&self, command: &str, shell: CommandShell) -> CommandRisk {
|
||||
let command = command.to_ascii_lowercase();
|
||||
let mut reasons = Vec::new();
|
||||
|
||||
let default_rules;
|
||||
let rules: Box<dyn Iterator<Item = &CommandRiskRule> + '_> = if self.include_defaults {
|
||||
default_rules = default_command_risk_rules();
|
||||
Box::new(default_rules.iter().chain(self.rules.iter()))
|
||||
} else {
|
||||
Box::new(self.rules.iter())
|
||||
};
|
||||
|
||||
for rule in rules {
|
||||
if rule.matches(&command, shell) && !reasons.contains(&rule.reason) {
|
||||
reasons.push(rule.reason.clone());
|
||||
}
|
||||
}
|
||||
|
||||
CommandRisk {
|
||||
level: if reasons.is_empty() {
|
||||
RiskLevel::Low
|
||||
} else {
|
||||
RiskLevel::High
|
||||
},
|
||||
reasons,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CommandRiskPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
level: RiskLevel::Low,
|
||||
reasons: Vec::new(),
|
||||
include_defaults: true,
|
||||
rules: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct CommandRiskRule {
|
||||
pub match_all: Vec<String>,
|
||||
pub reason: String,
|
||||
#[serde(default)]
|
||||
pub shell: Option<CommandShell>,
|
||||
}
|
||||
|
||||
impl CommandRiskRule {
|
||||
pub fn new(match_all: Vec<&str>, reason: &str, shell: Option<CommandShell>) -> Self {
|
||||
Self {
|
||||
match_all: match_all.into_iter().map(str::to_string).collect(),
|
||||
reason: reason.to_string(),
|
||||
shell,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches(&self, command: &str, shell: CommandShell) -> bool {
|
||||
if self.shell.is_some_and(|expected| expected != shell) {
|
||||
return false;
|
||||
}
|
||||
|
||||
!self.match_all.is_empty()
|
||||
&& self
|
||||
.match_all
|
||||
.iter()
|
||||
.all(|pattern| command.contains(&pattern.to_ascii_lowercase()))
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandRisk {
|
||||
pub fn warning(&self) -> Option<String> {
|
||||
if self.reasons.is_empty() {
|
||||
None
|
||||
|
|
@ -69,6 +157,13 @@ impl CommandRisk {
|
|||
}
|
||||
|
||||
pub fn parse_command_suggestions(response: &str) -> Vec<CommandSuggestion> {
|
||||
parse_command_suggestions_with_policy(response, &CommandRiskPolicy::default())
|
||||
}
|
||||
|
||||
pub fn parse_command_suggestions_with_policy(
|
||||
response: &str,
|
||||
policy: &CommandRiskPolicy,
|
||||
) -> Vec<CommandSuggestion> {
|
||||
let mut suggestions = Vec::new();
|
||||
let mut lines = response.lines().peekable();
|
||||
let mut previous_text = Vec::new();
|
||||
|
|
@ -107,7 +202,7 @@ pub fn parse_command_suggestions(response: &str) -> Vec<CommandSuggestion> {
|
|||
.rev()
|
||||
.find(|line| !line.contains("risk:") && !line.contains("[risk:"))
|
||||
.cloned();
|
||||
let detected_risk = detect_command_risk(&command, shell);
|
||||
let detected_risk = detect_command_risk_with_policy(&command, shell, policy);
|
||||
|
||||
suggestions.push(CommandSuggestion {
|
||||
id,
|
||||
|
|
@ -124,68 +219,15 @@ pub fn parse_command_suggestions(response: &str) -> Vec<CommandSuggestion> {
|
|||
}
|
||||
|
||||
pub fn detect_command_risk(command: &str, shell: CommandShell) -> CommandRisk {
|
||||
let lowered = command.to_ascii_lowercase();
|
||||
let mut reasons = Vec::new();
|
||||
detect_command_risk_with_policy(command, shell, &CommandRiskPolicy::default())
|
||||
}
|
||||
|
||||
if lowered.contains("rm -rf")
|
||||
|| lowered.contains("rm -fr")
|
||||
|| lowered.contains("remove-item")
|
||||
&& lowered.contains("-recurse")
|
||||
&& lowered.contains("-force")
|
||||
|| lowered.contains("del /s")
|
||||
{
|
||||
reasons.push("recursive or forced deletion".into());
|
||||
}
|
||||
if lowered.contains("format-volume")
|
||||
|| lowered.contains("format ")
|
||||
|| lowered.contains("mkfs")
|
||||
|| lowered.contains("diskpart")
|
||||
{
|
||||
reasons.push("disk formatting or partition operation".into());
|
||||
}
|
||||
if lowered.contains("chmod -r")
|
||||
|| lowered.contains("chown -r")
|
||||
|| lowered.contains("icacls ") && lowered.contains("/grant")
|
||||
{
|
||||
reasons.push("recursive permission change".into());
|
||||
}
|
||||
if lowered.contains("curl ") && lowered.contains("| sh")
|
||||
|| lowered.contains("wget ") && lowered.contains("| sh")
|
||||
|| lowered.contains("irm ") && lowered.contains("iex")
|
||||
|| lowered.contains("invoke-restmethod") && lowered.contains("invoke-expression")
|
||||
{
|
||||
reasons.push("downloaded content piped to an interpreter".into());
|
||||
}
|
||||
if lowered.contains("api_key")
|
||||
|| lowered.contains("apikey")
|
||||
|| lowered.contains("password")
|
||||
|| lowered.contains("secret")
|
||||
|| lowered.contains("token")
|
||||
{
|
||||
reasons.push("possible credential exposure".into());
|
||||
}
|
||||
if lowered.contains("apt remove")
|
||||
|| lowered.contains("apt purge")
|
||||
|| lowered.contains("dnf remove")
|
||||
|| lowered.contains("yum remove")
|
||||
|| lowered.contains("pacman -r")
|
||||
|| lowered.contains("uninstall-package")
|
||||
{
|
||||
reasons.push("package removal".into());
|
||||
}
|
||||
|
||||
if shell == CommandShell::PowerShell && lowered.contains("set-executionpolicy") {
|
||||
reasons.push("PowerShell execution policy change".into());
|
||||
}
|
||||
|
||||
CommandRisk {
|
||||
level: if reasons.is_empty() {
|
||||
RiskLevel::Low
|
||||
} else {
|
||||
RiskLevel::High
|
||||
},
|
||||
reasons,
|
||||
}
|
||||
pub fn detect_command_risk_with_policy(
|
||||
command: &str,
|
||||
shell: CommandShell,
|
||||
policy: &CommandRiskPolicy,
|
||||
) -> CommandRisk {
|
||||
policy.evaluate(command, shell)
|
||||
}
|
||||
|
||||
pub fn render_suggestions(suggestions: &[CommandSuggestion]) -> String {
|
||||
|
|
@ -241,6 +283,122 @@ fn parse_risk_marker(line: &str) -> Option<RiskLevel> {
|
|||
}
|
||||
}
|
||||
|
||||
fn default_command_risk_rules() -> Vec<CommandRiskRule> {
|
||||
vec![
|
||||
CommandRiskRule::new(
|
||||
vec!["rm -rf"],
|
||||
"recursive or forced deletion",
|
||||
Some(CommandShell::Posix),
|
||||
),
|
||||
CommandRiskRule::new(
|
||||
vec!["rm -fr"],
|
||||
"recursive or forced deletion",
|
||||
Some(CommandShell::Posix),
|
||||
),
|
||||
CommandRiskRule::new(
|
||||
vec!["remove-item", "-recurse", "-force"],
|
||||
"recursive or forced deletion",
|
||||
Some(CommandShell::PowerShell),
|
||||
),
|
||||
CommandRiskRule::new(vec!["del /s"], "recursive or forced deletion", None),
|
||||
CommandRiskRule::new(
|
||||
vec!["format-volume"],
|
||||
"disk formatting or partition operation",
|
||||
Some(CommandShell::PowerShell),
|
||||
),
|
||||
CommandRiskRule::new(
|
||||
vec!["format "],
|
||||
"disk formatting or partition operation",
|
||||
None,
|
||||
),
|
||||
CommandRiskRule::new(
|
||||
vec!["mkfs"],
|
||||
"disk formatting or partition operation",
|
||||
Some(CommandShell::Posix),
|
||||
),
|
||||
CommandRiskRule::new(
|
||||
vec!["diskpart"],
|
||||
"disk formatting or partition operation",
|
||||
None,
|
||||
),
|
||||
CommandRiskRule::new(
|
||||
vec!["chmod -r"],
|
||||
"recursive permission change",
|
||||
Some(CommandShell::Posix),
|
||||
),
|
||||
CommandRiskRule::new(
|
||||
vec!["chown -r"],
|
||||
"recursive permission change",
|
||||
Some(CommandShell::Posix),
|
||||
),
|
||||
CommandRiskRule::new(
|
||||
vec!["icacls ", "/grant"],
|
||||
"recursive permission change",
|
||||
Some(CommandShell::PowerShell),
|
||||
),
|
||||
CommandRiskRule::new(
|
||||
vec!["curl ", "| sh"],
|
||||
"downloaded content piped to an interpreter",
|
||||
Some(CommandShell::Posix),
|
||||
),
|
||||
CommandRiskRule::new(
|
||||
vec!["wget ", "| sh"],
|
||||
"downloaded content piped to an interpreter",
|
||||
Some(CommandShell::Posix),
|
||||
),
|
||||
CommandRiskRule::new(
|
||||
vec!["irm ", "iex"],
|
||||
"downloaded content piped to an interpreter",
|
||||
Some(CommandShell::PowerShell),
|
||||
),
|
||||
CommandRiskRule::new(
|
||||
vec!["invoke-restmethod", "invoke-expression"],
|
||||
"downloaded content piped to an interpreter",
|
||||
Some(CommandShell::PowerShell),
|
||||
),
|
||||
CommandRiskRule::new(vec!["api_key"], "possible credential exposure", None),
|
||||
CommandRiskRule::new(vec!["apikey"], "possible credential exposure", None),
|
||||
CommandRiskRule::new(vec!["password"], "possible credential exposure", None),
|
||||
CommandRiskRule::new(vec!["secret"], "possible credential exposure", None),
|
||||
CommandRiskRule::new(vec!["token"], "possible credential exposure", None),
|
||||
CommandRiskRule::new(
|
||||
vec!["apt remove"],
|
||||
"package removal",
|
||||
Some(CommandShell::Posix),
|
||||
),
|
||||
CommandRiskRule::new(
|
||||
vec!["apt purge"],
|
||||
"package removal",
|
||||
Some(CommandShell::Posix),
|
||||
),
|
||||
CommandRiskRule::new(
|
||||
vec!["dnf remove"],
|
||||
"package removal",
|
||||
Some(CommandShell::Posix),
|
||||
),
|
||||
CommandRiskRule::new(
|
||||
vec!["yum remove"],
|
||||
"package removal",
|
||||
Some(CommandShell::Posix),
|
||||
),
|
||||
CommandRiskRule::new(
|
||||
vec!["pacman -r"],
|
||||
"package removal",
|
||||
Some(CommandShell::Posix),
|
||||
),
|
||||
CommandRiskRule::new(
|
||||
vec!["uninstall-package"],
|
||||
"package removal",
|
||||
Some(CommandShell::PowerShell),
|
||||
),
|
||||
CommandRiskRule::new(
|
||||
vec!["set-executionpolicy"],
|
||||
"PowerShell execution policy change",
|
||||
Some(CommandShell::PowerShell),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -285,4 +443,61 @@ mod tests {
|
|||
RiskLevel::Low
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_policy_rules_extend_defaults() {
|
||||
let policy = CommandRiskPolicy {
|
||||
include_defaults: true,
|
||||
rules: vec![CommandRiskRule::new(
|
||||
vec!["kubectl delete", "--all"],
|
||||
"cluster-wide deletion",
|
||||
Some(CommandShell::Posix),
|
||||
)],
|
||||
};
|
||||
|
||||
let risk = detect_command_risk_with_policy(
|
||||
"kubectl delete pods --all",
|
||||
CommandShell::Posix,
|
||||
&policy,
|
||||
);
|
||||
|
||||
assert_eq!(risk.level, RiskLevel::High);
|
||||
assert_eq!(risk.reasons, vec!["cluster-wide deletion".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_policy_can_disable_defaults() {
|
||||
let policy = CommandRiskPolicy {
|
||||
include_defaults: false,
|
||||
rules: Vec::new(),
|
||||
};
|
||||
|
||||
let risk = detect_command_risk_with_policy("rm -rf build", CommandShell::Posix, &policy);
|
||||
|
||||
assert_eq!(risk.level, RiskLevel::Low);
|
||||
assert!(risk.reasons.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_uses_supplied_policy() {
|
||||
let policy = CommandRiskPolicy {
|
||||
include_defaults: false,
|
||||
rules: vec![CommandRiskRule::new(
|
||||
vec!["terraform apply"],
|
||||
"infrastructure mutation",
|
||||
Some(CommandShell::Posix),
|
||||
)],
|
||||
};
|
||||
|
||||
let suggestions = parse_command_suggestions_with_policy(
|
||||
"Apply infra:\n```sh\nterraform apply\n```",
|
||||
&policy,
|
||||
);
|
||||
|
||||
assert_eq!(suggestions[0].detected_risk.level, RiskLevel::High);
|
||||
assert_eq!(
|
||||
suggestions[0].detected_risk.reasons,
|
||||
vec!["infrastructure mutation".to_string()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use std::path::{Path, PathBuf};
|
|||
use serde::Deserialize;
|
||||
|
||||
use crate::app::CliOptions;
|
||||
use crate::commands::{CommandRiskPolicy, CommandRiskRule};
|
||||
use crate::context::ContextBudget;
|
||||
use crate::prompts::Stance;
|
||||
use crate::shell::ShellFamily;
|
||||
|
|
@ -14,6 +15,7 @@ pub struct Config {
|
|||
pub provider: ProviderConfig,
|
||||
pub shell: ShellConfig,
|
||||
pub interaction: InteractionConfig,
|
||||
pub commands: CommandConfig,
|
||||
pub transcript: TranscriptConfig,
|
||||
pub context: ContextConfig,
|
||||
}
|
||||
|
|
@ -37,6 +39,11 @@ pub struct InteractionConfig {
|
|||
pub stance: Stance,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CommandConfig {
|
||||
pub risk: CommandRiskPolicy,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TranscriptConfig {
|
||||
pub directory: PathBuf,
|
||||
|
|
@ -63,6 +70,7 @@ struct RawConfig {
|
|||
provider: Option<RawProviderConfig>,
|
||||
shell: Option<RawShellConfig>,
|
||||
interaction: Option<RawInteractionConfig>,
|
||||
commands: Option<RawCommandConfig>,
|
||||
transcript: Option<RawTranscriptConfig>,
|
||||
context: Option<RawContextConfig>,
|
||||
}
|
||||
|
|
@ -85,6 +93,17 @@ struct RawInteractionConfig {
|
|||
stance: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
struct RawCommandConfig {
|
||||
risk: Option<RawCommandRiskConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
struct RawCommandRiskConfig {
|
||||
include_defaults: Option<bool>,
|
||||
rules: Option<Vec<CommandRiskRule>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
struct RawTranscriptConfig {
|
||||
directory: Option<PathBuf>,
|
||||
|
|
@ -111,6 +130,7 @@ impl Config {
|
|||
let provider = raw.provider.unwrap_or_default();
|
||||
let shell = raw.shell.unwrap_or_default();
|
||||
let interaction = raw.interaction.unwrap_or_default();
|
||||
let commands = raw.commands.unwrap_or_default();
|
||||
let transcript = raw.transcript.unwrap_or_default();
|
||||
let context = raw.context.unwrap_or_default();
|
||||
|
||||
|
|
@ -131,6 +151,7 @@ impl Config {
|
|||
.unwrap_or_else(|| Stance::default().to_string())
|
||||
.parse::<Stance>()
|
||||
.map_err(|error| ConfigError::Invalid(error.to_string()))?;
|
||||
let risk = command_risk_policy(commands.risk)?;
|
||||
|
||||
Ok(Self {
|
||||
provider: ProviderConfig {
|
||||
|
|
@ -142,6 +163,7 @@ impl Config {
|
|||
},
|
||||
shell: ShellConfig { family },
|
||||
interaction: InteractionConfig { stance },
|
||||
commands: CommandConfig { risk },
|
||||
transcript: TranscriptConfig {
|
||||
directory: transcript.directory.unwrap_or_else(default_transcript_dir),
|
||||
enabled: transcript.enabled.unwrap_or(true),
|
||||
|
|
@ -174,6 +196,42 @@ impl Config {
|
|||
}
|
||||
}
|
||||
|
||||
fn command_risk_policy(
|
||||
raw: Option<RawCommandRiskConfig>,
|
||||
) -> Result<CommandRiskPolicy, ConfigError> {
|
||||
let Some(raw) = raw else {
|
||||
return Ok(CommandRiskPolicy::default());
|
||||
};
|
||||
|
||||
let rules = raw.rules.unwrap_or_default();
|
||||
for rule in &rules {
|
||||
if rule.match_all.is_empty() {
|
||||
return Err(ConfigError::Invalid(
|
||||
"commands.risk.rules entries require at least one match_all pattern".into(),
|
||||
));
|
||||
}
|
||||
if rule
|
||||
.match_all
|
||||
.iter()
|
||||
.any(|pattern| pattern.trim().is_empty())
|
||||
{
|
||||
return Err(ConfigError::Invalid(
|
||||
"commands.risk.rules match_all patterns cannot be empty".into(),
|
||||
));
|
||||
}
|
||||
if rule.reason.trim().is_empty() {
|
||||
return Err(ConfigError::Invalid(
|
||||
"commands.risk.rules reason cannot be empty".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CommandRiskPolicy {
|
||||
include_defaults: raw.include_defaults.unwrap_or(true),
|
||||
rules,
|
||||
})
|
||||
}
|
||||
|
||||
impl RawConfig {
|
||||
fn from_path(path: &Path) -> Result<Self, ConfigError> {
|
||||
let contents = fs::read_to_string(path).map_err(|error| ConfigError::Read {
|
||||
|
|
@ -328,6 +386,7 @@ mod tests {
|
|||
family: Some("cmd".into()),
|
||||
}),
|
||||
interaction: None,
|
||||
commands: None,
|
||||
transcript: None,
|
||||
context: None,
|
||||
})
|
||||
|
|
@ -353,6 +412,14 @@ family = "posix"
|
|||
[interaction]
|
||||
stance = "audit"
|
||||
|
||||
[commands.risk]
|
||||
include_defaults = true
|
||||
|
||||
[[commands.risk.rules]]
|
||||
match_all = ["kubectl delete", "--all"]
|
||||
reason = "cluster-wide deletion"
|
||||
shell = "posix"
|
||||
|
||||
[transcript]
|
||||
enabled = false
|
||||
|
||||
|
|
@ -371,6 +438,12 @@ max_estimated_tokens = 3000
|
|||
assert_eq!(config.provider.request_timeout_seconds, 45);
|
||||
assert_eq!(config.shell.family, ShellFamily::Posix);
|
||||
assert_eq!(config.interaction.stance, Stance::Audit);
|
||||
assert!(config.commands.risk.include_defaults);
|
||||
assert_eq!(config.commands.risk.rules.len(), 1);
|
||||
assert_eq!(
|
||||
config.commands.risk.rules[0].reason,
|
||||
"cluster-wide deletion"
|
||||
);
|
||||
assert!(!config.transcript.enabled);
|
||||
assert_eq!(config.context.max_characters, Some(12000));
|
||||
assert_eq!(config.context.max_estimated_tokens, Some(3000));
|
||||
|
|
@ -395,6 +468,29 @@ max_estimated_tokens = 3000
|
|||
assert_eq!(config.context.max_characters, None);
|
||||
assert_eq!(config.context.max_estimated_tokens, None);
|
||||
assert_eq!(config.provider.request_timeout_seconds, 120);
|
||||
assert!(config.commands.risk.include_defaults);
|
||||
assert!(config.commands.risk.rules.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_risk_defaults_can_be_disabled() {
|
||||
let mut file = tempfile::NamedTempFile::new().expect("temp config");
|
||||
write!(
|
||||
file,
|
||||
r#"
|
||||
[provider]
|
||||
base_url = "http://localhost:11434/v1"
|
||||
|
||||
[commands.risk]
|
||||
include_defaults = false
|
||||
"#
|
||||
)
|
||||
.expect("write config");
|
||||
|
||||
let config = Config::load(Some(file.path())).expect("config loads");
|
||||
|
||||
assert!(!config.commands.risk.include_defaults);
|
||||
assert!(config.commands.risk.rules.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
use crate::commands::{parse_command_suggestions, render_suggestions};
|
||||
use crate::commands::{
|
||||
CommandRiskPolicy, parse_command_suggestions_with_policy, render_suggestions,
|
||||
};
|
||||
|
||||
pub fn render_assistant_output(response: &str) -> String {
|
||||
render_assistant_output_with_policy(response, &CommandRiskPolicy::default())
|
||||
}
|
||||
|
||||
pub fn render_assistant_output_with_policy(response: &str, policy: &CommandRiskPolicy) -> String {
|
||||
let mut rendered = String::new();
|
||||
let mut in_command_block = false;
|
||||
|
||||
|
|
@ -27,7 +33,7 @@ pub fn render_assistant_output(response: &str) -> String {
|
|||
rendered.push('\n');
|
||||
}
|
||||
|
||||
let suggestions = parse_command_suggestions(response);
|
||||
let suggestions = parse_command_suggestions_with_policy(response, policy);
|
||||
if !suggestions.is_empty() {
|
||||
rendered.push_str(&render_suggestions(&suggestions));
|
||||
rendered.push('\n');
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use std::io::{self, Write};
|
||||
|
||||
use crate::app::{App, AppError};
|
||||
use crate::formatting::render_assistant_output;
|
||||
|
||||
pub struct Repl {
|
||||
app: App,
|
||||
|
|
@ -75,7 +74,7 @@ impl Repl {
|
|||
|
||||
println!("waiting for provider response...");
|
||||
match self.app.send(input).await {
|
||||
Ok(response) => println!("\n{}\n", render_assistant_output(&response)),
|
||||
Ok(response) => println!("\n{}\n", self.app.render_assistant_output(&response)),
|
||||
Err(error) => eprintln!("request failed: {error}"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user