Initial commit
This commit is contained in:
570
Cargo.lock
generated
Normal file
570
Cargo.lock
generated
Normal file
@@ -0,0 +1,570 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "agent-sandbox"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"glob",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip3"
|
||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "agent-sandbox"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
name = "agent_sandbox"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "agent-sandbox"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
glob = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
21
src/agents.rs
Normal file
21
src/agents.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn agent_rw_paths() -> Vec<PathBuf> {
|
||||
let home = match env::var("HOME") {
|
||||
Ok(h) => PathBuf::from(h),
|
||||
Err(_) => return vec![],
|
||||
};
|
||||
|
||||
let candidates = [
|
||||
env::var("CLAUDE_CONFIG_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| home.join(".claude")),
|
||||
env::var("CODEX_HOME")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| home.join(".codex")),
|
||||
home.join(".pi"),
|
||||
];
|
||||
|
||||
candidates.into_iter().filter(|p| p.is_dir()).collect()
|
||||
}
|
||||
267
src/blacklist.rs
Normal file
267
src/blacklist.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::SandboxError;
|
||||
|
||||
pub struct PathContext {
|
||||
pub home: String,
|
||||
pub run_user: String,
|
||||
}
|
||||
|
||||
pub struct BlacklistOverlays {
|
||||
pub tmpfs_dirs: Vec<PathBuf>,
|
||||
pub null_bind_files: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
pub fn resolve_overlays(ctx: &PathContext) -> Result<BlacklistOverlays, SandboxError> {
|
||||
let mut tmpfs_dirs = Vec::new();
|
||||
let mut null_bind_files = Vec::new();
|
||||
|
||||
for raw in SENSITIVE_PATHS {
|
||||
let expanded = expand_path(raw, ctx);
|
||||
for path in expand_glob(&expanded)? {
|
||||
match classify_path(&path) {
|
||||
PathKind::Dir => tmpfs_dirs.push(path),
|
||||
PathKind::File => {
|
||||
if !is_under_tmpfs_dir(&path, &tmpfs_dirs) {
|
||||
null_bind_files.push(path);
|
||||
}
|
||||
}
|
||||
PathKind::Missing => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(BlacklistOverlays {
|
||||
tmpfs_dirs,
|
||||
null_bind_files,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn resolve_path_context() -> Result<PathContext, SandboxError> {
|
||||
let home = std::env::var("HOME").map_err(|_| SandboxError::HomeNotSet)?;
|
||||
let run_user = std::env::var("XDG_RUNTIME_DIR")
|
||||
.unwrap_or_else(|_| resolve_run_user_from_proc().unwrap_or_else(|| "/run/user/0".into()));
|
||||
Ok(PathContext { home, run_user })
|
||||
}
|
||||
|
||||
enum PathKind {
|
||||
Dir,
|
||||
File,
|
||||
Missing,
|
||||
}
|
||||
|
||||
fn classify_path(path: &Path) -> PathKind {
|
||||
match fs::symlink_metadata(path) {
|
||||
Ok(m) if m.is_dir() => PathKind::Dir,
|
||||
Ok(_) => PathKind::File,
|
||||
Err(_) => PathKind::Missing,
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_path(raw: &str, ctx: &PathContext) -> String {
|
||||
let s = raw
|
||||
.replace("${HOME}", &ctx.home)
|
||||
.replace("${RUNUSER}", &ctx.run_user);
|
||||
let s = if let Some(rest) = s.strip_prefix('~') {
|
||||
format!("{}{rest}", ctx.home)
|
||||
} else {
|
||||
s
|
||||
};
|
||||
assert!(
|
||||
!s.contains("${"),
|
||||
"unexpanded variable in SENSITIVE_PATHS entry: {raw}"
|
||||
);
|
||||
s
|
||||
}
|
||||
|
||||
fn expand_glob(pattern: &str) -> Result<Vec<PathBuf>, SandboxError> {
|
||||
let entries = glob::glob(pattern)?;
|
||||
Ok(entries.filter_map(|r| r.ok()).collect())
|
||||
}
|
||||
|
||||
fn is_under_tmpfs_dir(path: &Path, tmpfs_dirs: &[PathBuf]) -> bool {
|
||||
tmpfs_dirs.iter().any(|dir| path.starts_with(dir))
|
||||
}
|
||||
|
||||
fn resolve_run_user_from_proc() -> Option<String> {
|
||||
let status = fs::read_to_string("/proc/self/status").ok()?;
|
||||
for line in status.lines() {
|
||||
if let Some(rest) = line.strip_prefix("Uid:") {
|
||||
let uid = rest.split_whitespace().next()?;
|
||||
return Some(format!("/run/user/{uid}"));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Curated sensitive paths from firejail disable-common.inc + disable-programs.inc.
|
||||
// Goal: protect secrets, credentials, and session tokens from agentic access.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SENSITIVE_PATHS: &[&str] = &[
|
||||
// -- history files (can leak passwords/tokens typed on command line) --
|
||||
"${HOME}/.*_history",
|
||||
"${HOME}/.*_history_*",
|
||||
"${HOME}/.histfile",
|
||||
"${HOME}/.history",
|
||||
"${HOME}/.python-history",
|
||||
"${HOME}/.pythonhist",
|
||||
"${HOME}/.viminfo",
|
||||
"${HOME}/.lesshst",
|
||||
// -- clipboard managers (may contain copied passwords) --
|
||||
"${HOME}/.cache/greenclip*",
|
||||
"${HOME}/.kde/share/apps/klipper",
|
||||
"${HOME}/.kde4/share/apps/klipper",
|
||||
"${HOME}/.local/share/klipper",
|
||||
"/tmp/clipmenu*",
|
||||
// -- SSH and remote access --
|
||||
"${HOME}/.ssh",
|
||||
"${HOME}/.rhosts",
|
||||
"${HOME}/.shosts",
|
||||
"/etc/hosts.equiv",
|
||||
"/etc/ssh",
|
||||
"/etc/ssh/*",
|
||||
// -- GPG --
|
||||
"${HOME}/.gnupg",
|
||||
// -- git credentials --
|
||||
"${HOME}/.git-credentials",
|
||||
"${HOME}/.git-credential-cache",
|
||||
"${HOME}/.config/hub",
|
||||
"${HOME}/.config/gh",
|
||||
// -- general credentials and secrets --
|
||||
"${HOME}/.netrc",
|
||||
"${HOME}/.cargo/credentials",
|
||||
"${HOME}/.cargo/credentials.toml",
|
||||
"${HOME}/.fetchmailrc",
|
||||
"${HOME}/.msmtprc",
|
||||
"${HOME}/.smbcredentials",
|
||||
"${HOME}/.davfs2/secrets",
|
||||
"${HOME}/.config/msmtp",
|
||||
"${HOME}/.config/keybase",
|
||||
"${HOME}/.minisign",
|
||||
"${HOME}/.caff",
|
||||
"${HOME}/.password-store",
|
||||
// -- cloud provider credentials --
|
||||
"${HOME}/.aws",
|
||||
"${HOME}/.boto",
|
||||
"${HOME}/.config/gcloud",
|
||||
"${HOME}/.kube",
|
||||
"${HOME}/.passwd-s3fs",
|
||||
"${HOME}/.s3cmd",
|
||||
"/etc/boto.cfg",
|
||||
// -- keyrings and wallets --
|
||||
"${HOME}/.gnome2/keyrings",
|
||||
"${HOME}/.local/share/keyrings",
|
||||
"${HOME}/.local/share/kwalletd",
|
||||
"${HOME}/.kde/share/apps/kwallet",
|
||||
"${HOME}/.kde4/share/apps/kwallet",
|
||||
// -- certificates and PKI --
|
||||
"${HOME}/.pki",
|
||||
"${HOME}/.cert",
|
||||
"${HOME}/.local/share/pki",
|
||||
"${HOME}/.local/share/plasma-vault",
|
||||
"${HOME}/.vaults",
|
||||
// -- KeePass databases --
|
||||
"${HOME}/*.kdb",
|
||||
"${HOME}/*.kdbx",
|
||||
// -- encryption --
|
||||
"${HOME}/.ecryptfs",
|
||||
"${HOME}/.fscrypt",
|
||||
"${HOME}/.Private",
|
||||
"${HOME}/Private",
|
||||
"/.fscrypt",
|
||||
"/home/.ecryptfs",
|
||||
"/home/.fscrypt",
|
||||
"/crypto_keyfile.bin",
|
||||
// -- system auth files --
|
||||
"/etc/shadow",
|
||||
"/etc/shadow+",
|
||||
"/etc/shadow-",
|
||||
"/etc/gshadow",
|
||||
"/etc/gshadow+",
|
||||
"/etc/gshadow-",
|
||||
"/etc/passwd+",
|
||||
"/etc/passwd-",
|
||||
"/etc/group+",
|
||||
"/etc/group-",
|
||||
"/etc/sudo*.conf",
|
||||
"/etc/sudoers*",
|
||||
"/etc/doas.conf",
|
||||
"/etc/davfs2/secrets",
|
||||
"/etc/msmtprc",
|
||||
// -- session directory and sockets (lateral movement vectors) --
|
||||
"/tmp/ssh-*",
|
||||
"/tmp/tmux-*",
|
||||
"${RUNUSER}",
|
||||
"/var/run/docker.sock",
|
||||
// -- mail (sensitive content) --
|
||||
"${HOME}/.Mail",
|
||||
"${HOME}/.mail",
|
||||
"${HOME}/Mail",
|
||||
"${HOME}/mail",
|
||||
"${HOME}/postponed",
|
||||
"${HOME}/sent",
|
||||
"${HOME}/.mutt",
|
||||
"${HOME}/.muttrc",
|
||||
// -- password managers --
|
||||
"${HOME}/.keepass",
|
||||
"${HOME}/.keepassx",
|
||||
"${HOME}/.keepassxc",
|
||||
"${HOME}/.config/KeePass",
|
||||
"${HOME}/.config/KeePassXCrc",
|
||||
"${HOME}/.config/keepassxc",
|
||||
"${HOME}/.cache/keepassxc",
|
||||
"${HOME}/.local/share/KeePass",
|
||||
"${HOME}/.config/1Password",
|
||||
"${HOME}/.config/Bitwarden",
|
||||
"${HOME}/.config/Enpass",
|
||||
"${HOME}/.cache/Enpass",
|
||||
"${HOME}/.local/share/Enpass",
|
||||
"${HOME}/.lastpass",
|
||||
"${HOME}/.config/Authenticator",
|
||||
"${HOME}/.cache/Authenticator",
|
||||
// -- browser profiles (saved passwords, cookies, session tokens) --
|
||||
"${HOME}/.mozilla",
|
||||
"${HOME}/.cache/mozilla",
|
||||
"${HOME}/.config/mozilla",
|
||||
"${HOME}/.config/google-chrome",
|
||||
"${HOME}/.cache/google-chrome",
|
||||
"${HOME}/.config/chromium",
|
||||
"${HOME}/.cache/chromium",
|
||||
"${HOME}/.config/BraveSoftware",
|
||||
"${HOME}/.cache/BraveSoftware",
|
||||
"${HOME}/.config/microsoft-edge",
|
||||
"${HOME}/.cache/microsoft-edge",
|
||||
"${HOME}/.config/vivaldi",
|
||||
"${HOME}/.cache/vivaldi",
|
||||
"${HOME}/.config/opera",
|
||||
"${HOME}/.cache/opera",
|
||||
"${HOME}/.librewolf",
|
||||
"${HOME}/.cache/librewolf",
|
||||
"${HOME}/.config/qutebrowser",
|
||||
"${HOME}/.cache/qutebrowser",
|
||||
"${HOME}/.local/opt/tor-browser",
|
||||
"${HOME}/.tor-browser*",
|
||||
"${HOME}/.cache/torbrowser",
|
||||
"${HOME}/.config/torbrowser",
|
||||
// -- cryptocurrency wallets --
|
||||
"${HOME}/.*coin",
|
||||
"${HOME}/.bitcoin",
|
||||
"${HOME}/.electrum*",
|
||||
"${HOME}/.ethereum",
|
||||
"${HOME}/Monero/wallets",
|
||||
"${HOME}/wallet.dat",
|
||||
// -- nyx (tor controller) --
|
||||
"${HOME}/.nyx",
|
||||
// -- D-Bus sockets (can execute commands via systemd) --
|
||||
"/run/dbus",
|
||||
"/var/run/dbus",
|
||||
// -- X11 / Wayland sockets (keystroke injection, screen capture) --
|
||||
"/tmp/.X11-unix",
|
||||
"/tmp/.ICE-unix",
|
||||
"/tmp/.XIM-unix",
|
||||
"${RUNUSER}/wayland-*",
|
||||
"${RUNUSER}/X11-display",
|
||||
];
|
||||
63
src/errors.rs
Normal file
63
src/errors.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SandboxError {
|
||||
HomeNotSet,
|
||||
BwrapNotFound,
|
||||
CommandNotFound(PathBuf),
|
||||
CommandNotExecutable(PathBuf),
|
||||
RwPathMissing(PathBuf),
|
||||
RoPathMissing(PathBuf),
|
||||
ChdirMissing(PathBuf),
|
||||
CurrentDirUnavailable(std::io::Error),
|
||||
GlobPattern(glob::PatternError),
|
||||
Io(std::io::Error),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SandboxError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::HomeNotSet => write!(
|
||||
f,
|
||||
"$HOME is not set; cannot determine which paths to protect"
|
||||
),
|
||||
Self::BwrapNotFound => write!(
|
||||
f,
|
||||
"bwrap not found; install bubblewrap (e.g. `apt install bubblewrap` or `pacman -S bubblewrap`)"
|
||||
),
|
||||
Self::CommandNotFound(p) => write!(f, "command not found: {}", p.display()),
|
||||
Self::CommandNotExecutable(p) => {
|
||||
write!(f, "command is not executable: {}", p.display())
|
||||
}
|
||||
Self::RwPathMissing(p) => write!(f, "--rw path does not exist: {}", p.display()),
|
||||
Self::RoPathMissing(p) => write!(f, "--ro path does not exist: {}", p.display()),
|
||||
Self::ChdirMissing(p) => write!(f, "--chdir path does not exist: {}", p.display()),
|
||||
Self::CurrentDirUnavailable(e) => write!(f, "cannot determine current directory: {e}"),
|
||||
Self::GlobPattern(e) => write!(f, "invalid glob pattern: {e}"),
|
||||
Self::Io(e) => write!(f, "I/O error: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for SandboxError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::CurrentDirUnavailable(e) => Some(e),
|
||||
Self::GlobPattern(e) => Some(e),
|
||||
Self::Io(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for SandboxError {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
Self::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<glob::PatternError> for SandboxError {
|
||||
fn from(e: glob::PatternError) -> Self {
|
||||
Self::GlobPattern(e)
|
||||
}
|
||||
}
|
||||
41
src/lib.rs
Normal file
41
src/lib.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
mod agents;
|
||||
mod blacklist;
|
||||
mod errors;
|
||||
mod preflight;
|
||||
mod sandbox;
|
||||
|
||||
pub use errors::SandboxError;
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub enum SandboxMode {
|
||||
Blacklist,
|
||||
Whitelist,
|
||||
}
|
||||
|
||||
pub struct SandboxConfig {
|
||||
pub mode: SandboxMode,
|
||||
pub hardened: bool,
|
||||
pub no_net: bool,
|
||||
pub extra_rw: Vec<PathBuf>,
|
||||
pub extra_ro: Vec<PathBuf>,
|
||||
pub command: PathBuf,
|
||||
pub command_args: Vec<OsString>,
|
||||
pub chdir: PathBuf,
|
||||
pub dry_run: bool,
|
||||
}
|
||||
|
||||
pub fn run(config: SandboxConfig) -> Result<(), SandboxError> {
|
||||
preflight::check(&config)?;
|
||||
|
||||
let mut cmd = sandbox::build_command(&config)?;
|
||||
|
||||
if config.dry_run {
|
||||
println!("{:?}", cmd);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(SandboxError::Io(cmd.exec()))
|
||||
}
|
||||
131
src/main.rs
Normal file
131
src/main.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use agent_sandbox::{SandboxConfig, SandboxMode};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
name = "agent-sandbox",
|
||||
version,
|
||||
about = "Sandbox agentic coding assistants with bubblewrap"
|
||||
)]
|
||||
struct Args {
|
||||
/// Blacklist mode: bind / read-only, overlay sensitive paths (default)
|
||||
#[arg(long, conflicts_with = "whitelist")]
|
||||
blacklist: bool,
|
||||
|
||||
/// Whitelist mode: only explicitly listed minimal paths visible
|
||||
#[arg(long)]
|
||||
whitelist: bool,
|
||||
|
||||
/// Harden: unshare IPC, PID, UTS; private /tmp, /dev, /run
|
||||
#[arg(long)]
|
||||
hardened: bool,
|
||||
|
||||
/// Unshare the network namespace
|
||||
#[arg(long)]
|
||||
no_net: bool,
|
||||
|
||||
/// Bind an extra path read-write (repeatable)
|
||||
#[arg(long = "rw", value_name = "PATH", action = clap::ArgAction::Append)]
|
||||
extra_rw: Vec<PathBuf>,
|
||||
|
||||
/// Bind an extra path read-only (repeatable)
|
||||
#[arg(long = "ro", value_name = "PATH", action = clap::ArgAction::Append)]
|
||||
extra_ro: Vec<PathBuf>,
|
||||
|
||||
/// Print the bwrap command without executing
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
|
||||
/// Working directory inside the sandbox (default: current directory)
|
||||
#[arg(long, value_name = "PATH")]
|
||||
chdir: Option<PathBuf>,
|
||||
|
||||
/// Command and arguments to run inside the sandbox
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
command_and_args: Vec<OsString>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
let (command, command_args) = resolve_command(args.command_and_args);
|
||||
let command = assert_binary_exists(&command);
|
||||
let chdir = assert_chdir(args.chdir);
|
||||
|
||||
let mode = if args.whitelist {
|
||||
SandboxMode::Whitelist
|
||||
} else {
|
||||
SandboxMode::Blacklist
|
||||
};
|
||||
|
||||
let config = SandboxConfig {
|
||||
mode,
|
||||
hardened: args.hardened,
|
||||
no_net: args.no_net,
|
||||
extra_rw: args.extra_rw,
|
||||
extra_ro: args.extra_ro,
|
||||
command,
|
||||
command_args,
|
||||
chdir,
|
||||
dry_run: args.dry_run,
|
||||
};
|
||||
|
||||
if let Err(e) = agent_sandbox::run(config) {
|
||||
eprintln!("error: {e}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_command(mut positional: Vec<OsString>) -> (OsString, Vec<OsString>) {
|
||||
if !positional.is_empty() {
|
||||
let cmd = positional.remove(0);
|
||||
return (cmd, positional);
|
||||
}
|
||||
if let Ok(cmd) = std::env::var("SANDBOX_CMD") {
|
||||
return (OsString::from(cmd), vec![]);
|
||||
}
|
||||
(
|
||||
OsString::from("claude"),
|
||||
vec![OsString::from("--dangerously-skip-permissions")],
|
||||
)
|
||||
}
|
||||
|
||||
fn assert_binary_exists(name: &OsStr) -> PathBuf {
|
||||
resolve_binary(name).unwrap_or_else(|| {
|
||||
eprintln!("error: command not found: {}", name.to_string_lossy());
|
||||
process::exit(1);
|
||||
})
|
||||
}
|
||||
|
||||
fn assert_chdir(explicit: Option<PathBuf>) -> PathBuf {
|
||||
if let Some(p) = explicit {
|
||||
return p;
|
||||
}
|
||||
match std::env::current_dir() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"error: {}",
|
||||
agent_sandbox::SandboxError::CurrentDirUnavailable(e)
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_binary(name: &OsStr) -> Option<PathBuf> {
|
||||
let path = PathBuf::from(name);
|
||||
if path.is_absolute() || path.components().count() > 1 {
|
||||
return path.is_file().then_some(path);
|
||||
}
|
||||
std::env::var_os("PATH").and_then(|path_var| {
|
||||
std::env::split_paths(&path_var)
|
||||
.map(|dir| dir.join(name))
|
||||
.find(|p| p.is_file())
|
||||
})
|
||||
}
|
||||
38
src/preflight.rs
Normal file
38
src/preflight.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::SandboxConfig;
|
||||
use crate::errors::SandboxError;
|
||||
|
||||
pub fn check(config: &SandboxConfig) -> Result<(), SandboxError> {
|
||||
check_bwrap()?;
|
||||
check_command(config)?;
|
||||
check_chdir(config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_chdir(config: &SandboxConfig) -> Result<(), SandboxError> {
|
||||
if !config.chdir.is_dir() {
|
||||
return Err(SandboxError::ChdirMissing(config.chdir.clone()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_bwrap() -> Result<(), SandboxError> {
|
||||
Command::new("bwrap")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map_err(|_| SandboxError::BwrapNotFound)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_command(config: &SandboxConfig) -> Result<(), SandboxError> {
|
||||
if !config.command.is_file() {
|
||||
return Err(SandboxError::CommandNotFound(config.command.clone()));
|
||||
}
|
||||
let metadata = std::fs::metadata(&config.command)?;
|
||||
if metadata.permissions().mode() & 0o111 == 0 {
|
||||
return Err(SandboxError::CommandNotExecutable(config.command.clone()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
132
src/sandbox.rs
Normal file
132
src/sandbox.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::agents;
|
||||
use crate::blacklist;
|
||||
use crate::{SandboxConfig, SandboxError, SandboxMode};
|
||||
|
||||
pub fn build_command(config: &SandboxConfig) -> Result<Command, SandboxError> {
|
||||
let mut cmd = Command::new("bwrap");
|
||||
let hardened = config.hardened || matches!(config.mode, SandboxMode::Whitelist);
|
||||
|
||||
if hardened {
|
||||
cmd.args(["--unshare-ipc", "--unshare-pid", "--unshare-uts"]);
|
||||
cmd.args(["--hostname", "sandbox"]);
|
||||
}
|
||||
if config.no_net {
|
||||
cmd.arg("--unshare-net");
|
||||
}
|
||||
|
||||
match config.mode {
|
||||
SandboxMode::Blacklist => add_blacklist_mode(&mut cmd)?,
|
||||
SandboxMode::Whitelist => add_whitelist_mode(&mut cmd)?,
|
||||
}
|
||||
|
||||
if hardened {
|
||||
cmd.args(["--tmpfs", "/tmp"]);
|
||||
cmd.args(["--dev", "/dev"]);
|
||||
cmd.args(["--tmpfs", "/run"]);
|
||||
cmd.args(["--proc", "/proc"]);
|
||||
}
|
||||
|
||||
for path in agents::agent_rw_paths() {
|
||||
cmd.arg("--bind").arg(&path).arg(&path);
|
||||
}
|
||||
|
||||
add_rw_bind(&mut cmd, &config.chdir)?;
|
||||
for path in &config.extra_rw {
|
||||
add_rw_bind(&mut cmd, path)?;
|
||||
}
|
||||
for path in &config.extra_ro {
|
||||
add_ro_bind(&mut cmd, path)?;
|
||||
}
|
||||
|
||||
cmd.arg("--die-with-parent");
|
||||
cmd.arg("--chdir").arg(&config.chdir);
|
||||
|
||||
cmd.arg("--")
|
||||
.arg(&config.command)
|
||||
.args(&config.command_args);
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
fn add_blacklist_mode(cmd: &mut Command) -> Result<(), SandboxError> {
|
||||
let ctx = blacklist::resolve_path_context()?;
|
||||
cmd.args(["--ro-bind", "/", "/"]);
|
||||
|
||||
let overlays = blacklist::resolve_overlays(&ctx)?;
|
||||
for dir in &overlays.tmpfs_dirs {
|
||||
cmd.arg("--tmpfs").arg(dir);
|
||||
}
|
||||
for file in &overlays.null_bind_files {
|
||||
cmd.arg("--ro-bind").arg("/dev/null").arg(file);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_whitelist_mode(cmd: &mut Command) -> Result<(), SandboxError> {
|
||||
let home = std::env::var("HOME").map_err(|_| SandboxError::HomeNotSet)?;
|
||||
|
||||
cmd.args(["--ro-bind", "/usr", "/usr"]);
|
||||
for path in ["/lib", "/lib64", "/lib32", "/bin", "/sbin"] {
|
||||
cmd.args(["--ro-bind-try", path, path]);
|
||||
}
|
||||
|
||||
for path in [
|
||||
"/etc/ld.so.cache",
|
||||
"/etc/ld.so.conf",
|
||||
"/etc/ld.so.conf.d",
|
||||
"/etc/alternatives",
|
||||
] {
|
||||
cmd.args(["--ro-bind-try", path, path]);
|
||||
}
|
||||
|
||||
cmd.args(["--ro-bind", "/etc/ssl", "/etc/ssl"]);
|
||||
cmd.args([
|
||||
"--ro-bind-try",
|
||||
"/etc/ca-certificates",
|
||||
"/etc/ca-certificates",
|
||||
]);
|
||||
cmd.args(["--ro-bind", "/etc/resolv.conf", "/etc/resolv.conf"]);
|
||||
cmd.args(["--ro-bind", "/etc/nsswitch.conf", "/etc/nsswitch.conf"]);
|
||||
cmd.args(["--ro-bind", "/etc/passwd", "/etc/passwd"]);
|
||||
cmd.args(["--ro-bind", "/etc/group", "/etc/group"]);
|
||||
|
||||
for path in [
|
||||
"/etc/hosts",
|
||||
"/etc/gai.conf",
|
||||
"/etc/services",
|
||||
"/etc/protocols",
|
||||
] {
|
||||
cmd.args(["--ro-bind-try", path, path]);
|
||||
}
|
||||
|
||||
for path in ["/etc/hostname", "/etc/localtime", "/etc/machine-id"] {
|
||||
cmd.args(["--ro-bind-try", path, path]);
|
||||
}
|
||||
|
||||
let local_bin = format!("{home}/.local/bin");
|
||||
cmd.arg("--ro-bind-try").arg(&local_bin).arg(&local_bin);
|
||||
|
||||
let cache_dir = format!("{home}/.cache");
|
||||
cmd.arg("--tmpfs").arg(&cache_dir);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_rw_bind(cmd: &mut Command, path: &Path) -> Result<(), SandboxError> {
|
||||
if !path.exists() {
|
||||
return Err(SandboxError::RwPathMissing(path.to_path_buf()));
|
||||
}
|
||||
cmd.arg("--bind").arg(path).arg(path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_ro_bind(cmd: &mut Command, path: &Path) -> Result<(), SandboxError> {
|
||||
if !path.exists() {
|
||||
return Err(SandboxError::RoPathMissing(path.to_path_buf()));
|
||||
}
|
||||
cmd.arg("--ro-bind").arg(path).arg(path);
|
||||
Ok(())
|
||||
}
|
||||
220
tests/integration.rs
Normal file
220
tests/integration.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn sandbox(extra_args: &[&str]) -> Command {
|
||||
let mut cmd = Command::new(env!("CARGO_BIN_EXE_agent-sandbox"));
|
||||
cmd.args(extra_args);
|
||||
cmd
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cwd_is_writable() {
|
||||
let output = sandbox(&[])
|
||||
.args(["--", "bash", "-c", "touch ./sandbox_canary && echo ok"])
|
||||
.output()
|
||||
.expect("agent-sandbox binary failed to execute");
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(
|
||||
stdout.contains("ok"),
|
||||
"expected 'ok' in stdout, got: {stdout}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_fs_is_readonly() {
|
||||
let output = sandbox(&[])
|
||||
.args(["--", "bash", "-c", "touch /etc/pwned 2>&1 || echo readonly"])
|
||||
.output()
|
||||
.expect("agent-sandbox binary failed to execute");
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(
|
||||
stdout.contains("readonly"),
|
||||
"expected 'readonly' in stdout, got: {stdout}"
|
||||
);
|
||||
assert!(!std::path::Path::new("/etc/pwned").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_dir_is_hidden() {
|
||||
let output = sandbox(&[])
|
||||
.args(["--", "bash", "-c", "ls ~/.ssh 2>/dev/null | wc -l"])
|
||||
.output()
|
||||
.expect("agent-sandbox binary failed to execute");
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
assert_eq!(stdout, "0", "expected empty ~/.ssh, got {stdout} entries");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_net_blocks_network() {
|
||||
let output = sandbox(&["--no-net"])
|
||||
.args([
|
||||
"--",
|
||||
"bash",
|
||||
"-c",
|
||||
"curl -s --max-time 2 http://1.1.1.1 2>&1; echo $?",
|
||||
])
|
||||
.output()
|
||||
.expect("agent-sandbox binary failed to execute");
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(
|
||||
!stdout.trim().ends_with("0"),
|
||||
"expected curl to fail, got: {stdout}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hardened_pid_namespace() {
|
||||
let output = sandbox(&["--hardened"])
|
||||
.args(["--", "bash", "-c", "ls /proc | grep -cE '^[0-9]+$'"])
|
||||
.output()
|
||||
.expect("agent-sandbox binary failed to execute");
|
||||
|
||||
let count: u32 = String::from_utf8_lossy(&output.stdout)
|
||||
.trim()
|
||||
.parse()
|
||||
.unwrap_or(999);
|
||||
assert!(
|
||||
count < 10,
|
||||
"expected isolated PID namespace with few PIDs, got {count}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitelist_hides_home_contents() {
|
||||
let output = sandbox(&["--whitelist"])
|
||||
.args(["--", "bash", "-c", "ls ~/Documents 2>&1 || echo hidden"])
|
||||
.output()
|
||||
.expect("agent-sandbox binary failed to execute");
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(
|
||||
stdout.contains("hidden"),
|
||||
"expected ~/Documents to be hidden, got: {stdout}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extra_ro_mount() {
|
||||
let dir = TempDir::new().expect("failed to create temp dir");
|
||||
fs::write(dir.path().join("hello.txt"), "hi").expect("failed to write test file");
|
||||
let dir_str = dir.path().to_str().unwrap();
|
||||
|
||||
let output = sandbox(&["--ro", dir_str])
|
||||
.args([
|
||||
"--",
|
||||
"bash",
|
||||
"-c",
|
||||
&format!("cat {dir_str}/hello.txt && touch {dir_str}/new 2>&1 || echo readonly"),
|
||||
])
|
||||
.output()
|
||||
.expect("agent-sandbox binary failed to execute");
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(
|
||||
stdout.contains("hi"),
|
||||
"expected file content 'hi', got: {stdout}"
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("readonly"),
|
||||
"expected ro mount to block writes, got: {stdout}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extra_rw_mount() {
|
||||
let dir = TempDir::new().expect("failed to create temp dir");
|
||||
let dir_str = dir.path().to_str().unwrap();
|
||||
|
||||
let output = sandbox(&["--rw", dir_str])
|
||||
.args([
|
||||
"--",
|
||||
"bash",
|
||||
"-c",
|
||||
&format!("touch {dir_str}/canary && echo ok"),
|
||||
])
|
||||
.output()
|
||||
.expect("agent-sandbox binary failed to execute");
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("ok"), "expected 'ok', got: {stdout}");
|
||||
assert!(
|
||||
dir.path().join("canary").exists(),
|
||||
"canary file should exist on host after rw mount"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chdir_override() {
|
||||
let dir = TempDir::new().expect("failed to create temp dir");
|
||||
let dir_str = dir.path().to_str().unwrap();
|
||||
|
||||
let output = sandbox(&["--chdir", dir_str])
|
||||
.args(["--", "bash", "-c", "pwd"])
|
||||
.output()
|
||||
.expect("agent-sandbox binary failed to execute");
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
assert_eq!(
|
||||
stdout, dir_str,
|
||||
"expected cwd to be {dir_str}, got: {stdout}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chdir_under_hardened_tmp() {
|
||||
let dir = TempDir::new().expect("failed to create temp dir");
|
||||
let dir_str = dir.path().to_str().unwrap();
|
||||
|
||||
let output = sandbox(&["--hardened", "--chdir", dir_str])
|
||||
.args(["--", "bash", "-c", "pwd && touch ./ok && echo done"])
|
||||
.output()
|
||||
.expect("agent-sandbox binary failed to execute");
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(
|
||||
stdout.contains("done"),
|
||||
"expected chdir under /tmp to work with --hardened, got: {stdout}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dry_run_prints_and_exits() {
|
||||
let output = sandbox(&["--dry-run"])
|
||||
.args(["--", "bash", "-c", "exit 42"])
|
||||
.output()
|
||||
.expect("agent-sandbox binary failed to execute");
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(
|
||||
stdout.contains("bwrap"),
|
||||
"expected bwrap command in dry-run output, got: {stdout}"
|
||||
);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"dry-run should exit 0, not 42 from the inner command"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rw_missing_path_errors() {
|
||||
let output = sandbox(&["--rw", "/nonexistent/xyz"])
|
||||
.args(["--", "true"])
|
||||
.output()
|
||||
.expect("agent-sandbox binary failed to execute");
|
||||
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"expected non-zero exit for missing --rw path"
|
||||
);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
stderr.contains("/nonexistent/xyz"),
|
||||
"expected path in error message, got: {stderr}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user