diff --git a/Cargo.lock b/Cargo.lock index a3de235..8f61549 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,6 +92,9 @@ dependencies = [ "agent-bridle-core", "anyhow", "async-trait", + "brush-ocap-builtins", + "brush-ocap-core", + "brush-ocap-coreutils-builtins", "serde", "serde_json", "tokio", @@ -134,6 +137,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -143,12 +158,95 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi-width" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219e3ce6f2611d83b51ec2098a12702112c29e57203a6b0a0929b2cddb486608" +dependencies = [ + "unicode-width 0.1.14", +] + +[[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 0.61.2", +] + +[[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 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "archery" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e0a5f99dfebb87bb342d0f53bb92c81842e100bbb915223e38349580e5441d" +dependencies = [ + "triomphe", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -204,6 +302,17 @@ dependencies = [ "event-listener", ] +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -245,6 +354,19 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bigdecimal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -260,6 +382,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.12.1" @@ -289,6 +417,134 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bon" +version = "3.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a602c73c7b0148ec6d12af6fd5cc7a46e2eacc8878271a999abac56eed12f561" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f" +dependencies = [ + "darling 0.23.0", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "brush-ocap-builtins" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22acca2e023904e6ddf726af11e5977d9636a71cde6942e1d9928361753ebd5" +dependencies = [ + "brush-ocap-core", + "brush-parser", + "cfg-if", + "chrono", + "clap", + "fancy-regex", + "itertools", + "nix", + "procfs", + "rlimit", + "strum", + "thiserror 2.0.18", + "tokio", + "tracing", + "uucore", +] + +[[package]] +name = "brush-ocap-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9590361ea19dd8581fa80d40b41ee19707bf9ba82c1f9c7433325974a0248e74" +dependencies = [ + "async-recursion", + "async-trait", + "bon", + "brush-parser", + "cached", + "cfg-if", + "check_elevation", + "chrono", + "clap", + "color-print", + "command-fds", + "fancy-regex", + "futures", + "getrandom 0.4.3", + "hostname", + "inherent", + "itertools", + "libc", + "nix", + "normalize-path", + "rand 0.10.2", + "rpds", + "strum", + "strum_macros", + "terminfo", + "thiserror 2.0.18", + "tokio", + "tracing", + "uuid", + "uzers", + "whoami", +] + +[[package]] +name = "brush-ocap-coreutils-builtins" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6fb36f78326cd82cd53e02f9a387f70ab6314bb75beb3ad4b9b1ca8727f21a" +dependencies = [ + "uu_cat", + "uu_echo", + "uu_ls", + "uucore", +] + +[[package]] +name = "brush-parser" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f64b19efc02f0dd6cac1d462e20a1098f425f09cb13624efbb1b7d63f061735" +dependencies = [ + "bon", + "cached", + "getrandom 0.4.3", + "indenter", + "insta", + "peg", + "thiserror 2.0.18", + "tracing", + "utf8-chars", + "uuid", +] + +[[package]] +name = "bstr" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -304,6 +560,40 @@ dependencies = [ "serde", ] +[[package]] +name = "cached" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b6f5d101f0f6322c8646a45b7c581a673e476329040d97565815c2461dd0c4" +dependencies = [ + "ahash", + "cached_proc_macro", + "cached_proc_macro_types", + "hashbrown 0.16.1", + "once_cell", + "parking_lot", + "thiserror 2.0.18", + "web-time", +] + +[[package]] +name = "cached_proc_macro" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ebcf9c75f17a17d55d11afc98e46167d4790a263f428891b8705ab2f793eca3" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" + [[package]] name = "cc" version = "1.2.63" @@ -326,6 +616,39 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "check_elevation" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7fe1fbbd8bd3174109071530f12932ff471eeff30d83e9c485d6ff424ac534b" +dependencies = [ + "windows", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "cipher" version = "0.4.4" @@ -336,6 +659,84 @@ dependencies = [ "inout", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +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", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +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 = "color-print" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aa954171903797d5623e047d9ab69d91b493657917bdfb8c2c80ecaf9cdb6f4" +dependencies = [ + "color-print-proc-macro", +] + +[[package]] +name = "color-print-proc-macro" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692186b5ebe54007e45a59aea47ece9eb4108e141326c304cdc91699a7118a22" +dependencies = [ + "nom 7.1.3", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "command-fds" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b60b5124979fccd9addd89d8b97a1d6eebb4950694520c75ddd722535ea443f" +dependencies = [ + "nix", + "thiserror 2.0.18", +] + [[package]] name = "compression-codecs" version = "0.4.38" @@ -362,6 +763,17 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" +dependencies = [ + "encode_unicode", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -374,6 +786,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -438,7 +856,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf", + "phf 0.13.1", "smallvec", ] @@ -479,12 +897,112 @@ dependencies = [ "syn", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn", +] + [[package]] name = "data-encoding" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "der" version = "0.7.10" @@ -550,7 +1068,7 @@ dependencies = [ "cssparser", "foldhash", "html5ever", - "nom", + "nom 8.0.0", "precomputed-hash", "selectors", "tendril", @@ -568,7 +1086,7 @@ dependencies = [ "gjson", "html-escape", "once_cell", - "phf", + "phf 0.13.1", "tendril", "thiserror 2.0.18", "unicode-segmentation", @@ -589,6 +1107,12 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "ecdsa" version = "0.16.9" @@ -629,6 +1153,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + [[package]] name = "elliptic-curve" version = "0.13.8" @@ -648,6 +1178,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -726,6 +1262,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fancy-regex" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -770,6 +1317,51 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fluent" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8137a6d5a2c50d6b0ebfcb9aaa91a28154e0a70605f112d30cb0cd4a78670477" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash", + "self_cell", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" +dependencies = [ + "memchr", + "thiserror 2.0.18", +] + [[package]] name = "fnv" version = "1.0.7" @@ -791,6 +1383,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -798,6 +1405,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -806,6 +1414,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.32" @@ -847,9 +1466,13 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] @@ -874,7 +1497,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -887,17 +1510,37 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + [[package]] name = "gjson" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43503cc176394dd30a6525f5f36e838339b8b5619be33ed9a7783841580a97b6" +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "group" version = "0.13.0" @@ -928,6 +1571,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -1024,6 +1678,17 @@ dependencies = [ "digest", ] +[[package]] +name = "hostname" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" +dependencies = [ + "cfg-if", + "libc", + "windows-link", +] + [[package]] name = "htmd" version = "0.5.4" @@ -1032,7 +1697,7 @@ checksum = "7eee9b00ee2e599b4f86507157e3db786e7a3319fc225f0e9584151dbea2291d" dependencies = [ "html5ever", "markup5ever_rcdom", - "phf", + "phf 0.13.1", ] [[package]] @@ -1194,6 +1859,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -1208,6 +1897,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_locale" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5a396343c7208121dc86e35623d3dfe19814a7613cfd14964994cdc9c9a2e26" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_locale_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + [[package]] name = "icu_locale_core" version = "2.2.0" @@ -1216,11 +1920,18 @@ checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", + "serde", "tinystr", "writeable", "zerovec", ] +[[package]] +name = "icu_locale_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fdcc9ac77c6d74ff5cf6e65ef3181d6af32003b16fce3a77fb451d2f695993" + [[package]] name = "icu_normalizer" version = "2.2.0" @@ -1269,6 +1980,8 @@ checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", + "serde", + "stable_deref_trait", "writeable", "yoke", "zerofrom", @@ -1276,6 +1989,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1298,22 +2017,73 @@ dependencies = [ ] [[package]] -name = "indexmap" -version = "2.14.0" +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "inherent" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "insta" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" +dependencies = [ + "console", + "once_cell", + "pest", + "pest_derive", + "serde", + "similar", + "tempfile", +] + +[[package]] +name = "intl-memoizer" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" dependencies = [ - "equivalent", - "hashbrown", + "type-map", + "unic-langid", ] [[package]] -name = "inout" -version = "0.1.4" +name = "intl_pluralrules" +version = "7.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" dependencies = [ - "generic-array", + "unic-langid", ] [[package]] @@ -1335,12 +2105,69 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jiff" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccfe6121cbe750cf81efa362d85c0bde7ea298ec43092d3a193baca59cdbd634" +dependencies = [ + "defmt", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-static" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e165e897f662d428f3cd3828a919dbe067c2d42bb1031eede74ef9d27ecdedd2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6142247df1a93c2b3587402a19710be3e6e942f1581a1702e76408f2c21d6590" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "js-sys" version = "0.3.99" @@ -1385,12 +2212,27 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + [[package]] name = "linked-hash-map" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.2" @@ -1427,6 +2269,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lscolors" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d60e266dfb1426eb2d24792602e041131fdc0236bb7007abc0e589acafd60929" +dependencies = [ + "aho-corasick", + "nu-ansi-term", +] + [[package]] name = "markup5ever" version = "0.38.0" @@ -1462,6 +2314,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1479,7 +2337,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] @@ -1489,6 +2347,28 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.12.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nom" version = "8.0.0" @@ -1498,6 +2378,31 @@ dependencies = [ "memchr", ] +[[package]] +name = "normalize-path" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5438dd2b2ff4c6df6e1ce22d825ed2fa93ee2922235cc45186991717f0a892d" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -1544,12 +2449,45 @@ dependencies = [ "libm", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.12.1", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "objc2-core-foundation", +] + [[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 = "os_display" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5fd71b79026fb918650dde6d125000a233764f1c2f1659a1c71118e33ea08f" +dependencies = [ + "unicode-width 0.2.2", +] + [[package]] name = "p256" version = "0.13.2" @@ -1626,6 +2564,33 @@ dependencies = [ "smallvec", ] +[[package]] +name = "peg" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aad070be5b63aa72103f2fcdd70a83adbd5e90112ce5b574171ff1c65501773" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd8ef6825cae95355031ae26a99b616a2a21f22ba2de0197c43dfb05acbe7ee" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7011d97b484a5ebdc4b1fdb3b12d5e4bbbea56e9d22b688f2e79e04b65a7d8a6" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1641,6 +2606,57 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47627dd7305c6a2d6c8c6bcd24c5a4c17dbbf425f4f9c5313e724b38fc9782e9" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b4254325ecad416ab689e27ba51da03ba01a9632bc6e108f5fe7c3c4ad29d58" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c4c0e91ead7a8f7acecbca6f003fc2e8282b1dbe2dd9c9d2f16aba42995e0a7" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9744bc48116fee06334924bb5f2bad41eed5e89bd26e29b0b799f9a3f82c210" +dependencies = [ + "pest", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared 0.11.3", +] + [[package]] name = "phf" version = "0.13.1" @@ -1648,18 +2664,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ "phf_macros", - "phf_shared", + "phf_shared 0.13.1", "serde", ] +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + [[package]] name = "phf_codegen" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.6", ] [[package]] @@ -1669,7 +2705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ "fastrand", - "phf_shared", + "phf_shared 0.13.1", ] [[package]] @@ -1678,13 +2714,22 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.13.1", + "phf_shared 0.13.1", "proc-macro2", "quote", "syn", ] +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "phf_shared" version = "0.13.1" @@ -1727,12 +2772,23 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ + "serde_core", + "writeable", "zerovec", ] @@ -1751,6 +2807,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[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 = "primeorder" version = "0.13.6" @@ -1769,6 +2835,30 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "procfs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7" +dependencies = [ + "bitflags 2.12.1", + "chrono", + "flate2", + "procfs-core", + "rustix", +] + +[[package]] +name = "procfs-core" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405" +dependencies = [ + "bitflags 2.12.1", + "chrono", + "hex", +] + [[package]] name = "pyo3" version = "0.28.3" @@ -1897,6 +2987,12 @@ 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.8.6" @@ -1918,6 +3014,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -1956,13 +3063,19 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.12.1", ] [[package]] @@ -2064,6 +3177,25 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rlimit" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f35ee2729c56bb610f6dba436bf78135f728b7373bdffae2ec815b2d3eb98cc3" +dependencies = [ + "libc", +] + +[[package]] +name = "rpds" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e025feb26210bc196b908e72deb063b1b4000754304341cbc168a1e72c857ebc" +dependencies = [ + "archery", + "smallvec", +] + [[package]] name = "rsa" version = "0.9.10" @@ -2100,6 +3232,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.12.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.40" @@ -2173,19 +3318,25 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ - "bitflags", + "bitflags 2.12.1", "cssparser", "derive_more", "log", "new_debug_unreachable", - "phf", - "phf_codegen", + "phf 0.13.1", + "phf_codegen 0.13.1", "precomputed-hash", "rustc-hash", "servo_arc", "smallvec", ] +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + [[package]] name = "semver" version = "1.0.28" @@ -2445,7 +3596,7 @@ checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared", + "phf_shared 0.13.1", "precomputed-hash", "serde", ] @@ -2456,8 +3607,8 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.13.1", + "phf_shared 0.13.1", "proc-macro2", "quote", ] @@ -2468,6 +3619,30 @@ version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b3c8667cd96245cbb600b8dec5680a7319edd719c5aa2b5d23c6bff94f39765" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2511,7 +3686,7 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fce91f2f0ec87dff7e6bcbbeb267439aa1188703003c6055193c821487400432" dependencies = [ - "unicode-width", + "unicode-width 0.2.2", ] [[package]] @@ -2520,6 +3695,19 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "tendril" version = "0.5.0" @@ -2530,6 +3718,28 @@ dependencies = [ "utf-8", ] +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom 7.1.3", + "phf 0.11.3", + "phf_codegen 0.11.3", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2577,6 +3787,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] @@ -2708,7 +3919,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "async-compression", - "bitflags", + "bitflags 2.12.1", "bytes", "futures-core", "futures-util", @@ -2768,18 +3979,57 @@ dependencies = [ "once_cell", ] +[[package]] +name = "triomphe" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b40688ea6389c8171614b25491f71d4a27946e0c7ce2da1c6de27e25abf1a0ae" + [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash", +] + [[package]] name = "typenum" version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "tinystr", +] + [[package]] name = "unicode-ident" version = "1.0.24" @@ -2792,12 +4042,24 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + [[package]] name = "untrusted" version = "0.9.0" @@ -2822,6 +4084,15 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-chars" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92498c67ea3511b37eccff13b573ed871faf0ce9c4056ab032bd01a681f445a0" +dependencies = [ + "arrayvec", +] + [[package]] name = "utf8-width" version = "0.1.8" @@ -2834,6 +4105,131 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uu_cat" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8c8b09c26aa08c010112715ff724a76fc8a8497c615830ca732e3ebe5c8429" +dependencies = [ + "clap", + "fluent", + "memchr", + "rustix", + "thiserror 2.0.18", + "uucore", + "winapi-util", + "windows-sys 0.61.2", +] + +[[package]] +name = "uu_echo" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d20e00a1dc54fef5e26704b35aa8837cb4416120f8a46415b9bdd9f8354070f0" +dependencies = [ + "clap", + "fluent", + "uucore", +] + +[[package]] +name = "uu_ls" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09df7fe9d6f628b78e05af31572f54731aeda28202f8dcbf9692db5744404056" +dependencies = [ + "ansi-width", + "clap", + "fluent", + "glob", + "hostname", + "lscolors", + "rustc-hash", + "terminal_size", + "thiserror 2.0.18", + "uucore", + "uutils_term_grid", +] + +[[package]] +name = "uucore" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07d779636d827cde4100f0e65ff3fd23b0b1f1195055475c6e6813d425f30c8e" +dependencies = [ + "bigdecimal", + "bstr", + "clap", + "dunce", + "fluent", + "fluent-bundle", + "fluent-syntax", + "glob", + "icu_locale", + "itertools", + "jiff", + "libc", + "nix", + "num-traits", + "os_display", + "procfs", + "rustc-hash", + "rustix", + "thiserror 2.0.18", + "unic-langid", + "unit-prefix", + "uucore_procs", + "wild", + "winapi-util", + "windows-sys 0.61.2", + "xattr", +] + +[[package]] +name = "uucore_procs" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da865e8a648b260dbd89fca76d0801995877ab27a4e259b6dec30ba9743b86ab" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "uuid" +version = "1.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "uutils_term_grid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382d49b39de4a115f203305057741126b09a615892d773a2d419a2b816e30e39" +dependencies = [ + "ansi-width", +] + +[[package]] +name = "uzers" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8275fb1afee25b4111d2dc8b5c505dbbc4afd0b990cb96deb2d88bff8be18d" +dependencies = [ + "libc", + "log", +] + [[package]] name = "version_check" version = "0.9.5" @@ -2855,6 +4251,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + [[package]] name = "wasip2" version = "1.0.3+wasi-0.2.9" @@ -2864,6 +4269,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasite" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" +dependencies = [ + "wasi 0.14.7+wasi-0.2.4", +] + [[package]] name = "wasm-bindgen" version = "0.2.122" @@ -2945,8 +4359,8 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" dependencies = [ - "phf", - "phf_codegen", + "phf 0.13.1", + "phf_codegen 0.13.1", "string_cache", "string_cache_codegen", ] @@ -2960,12 +4374,97 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "whoami" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" +dependencies = [ + "libc", + "libredox", + "objc2-system-configuration", + "wasite", + "web-sys", +] + [[package]] name = "widestring" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" +[[package]] +name = "wild" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3131afc8c575281e1e80f36ed6a092aa502c08b18ed7524e86fbbb12bb410e1" +dependencies = [ + "glob", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core 0.51.1", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -3037,6 +4536,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -3070,6 +4584,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -3082,6 +4602,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -3094,6 +4620,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3118,6 +4650,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -3130,6 +4668,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -3142,6 +4686,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -3154,6 +4704,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -3187,6 +4743,16 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "xml5ever" version = "0.38.0" @@ -3290,6 +4856,7 @@ dependencies = [ "displaydoc", "yoke", "zerofrom", + "zerovec", ] [[package]] @@ -3298,6 +4865,7 @@ version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", diff --git a/agent-bridle-tool-shell/Cargo.toml b/agent-bridle-tool-shell/Cargo.toml index d1d87d3..5c95345 100644 --- a/agent-bridle-tool-shell/Cargo.toml +++ b/agent-bridle-tool-shell/Cargo.toml @@ -19,6 +19,15 @@ authors.workspace = true name = "bridle-netmon" required-features = ["shell"] +# A dispatch-capable host binary for the carried-coreutils integration test +# (issue #206): its `main` calls `maybe_dispatch()` first, so the brush engine's +# coreutils shims (which re-exec ` --invoke-bundled `) resolve +# in-process against THIS binary — proving carried `ls`/`cat` work with no host +# tools. Only built with `carried-coreutils`. +[[bin]] +name = "dispatch_host" +required-features = ["carried-coreutils"] + [features] # `shell` is on by default so agent-bridle's registry() includes the tool. # It pulls tokio (spawn_blocking + timeout) for confined execution. @@ -29,6 +38,18 @@ shell = ["dep:tokio"] # Opt-in; the embedder registers it instead of the safe-subset ShellTool. Pulls # tokio (spawn_blocking) like `shell`; independent of the safe-subset modules. host-shell = ["dep:tokio"] +# The carried **brush** engine (agent-bridle#20 / Track 2): a bash-in-Rust shell +# run in-process, confined by the `CommandInterceptor` L2 leash — the only engine +# that also confines a *restricted* exec/net grant, and cross-platform. Uses the +# temporary `brush-ocap-*` fork, published to crates.io (reubeno/brush#1184 not +# yet upstream). Off by default; the crate stays publish-clean with `brush` off. +brush = ["dep:tokio", "dep:brush-ocap-core", "dep:brush-ocap-builtins"] +# Carried coreutils (agent-bridle#20 / issue #206): the brush engine registers +# bundled uu_* coreutils shims that re-exec ` --invoke-bundled `, so +# an embedded shell runs `ls`/`cat`/… with NO host tools — IF the host binary is +# dispatch-capable (calls `maybe_dispatch()` at the top of `main`). Off by +# default; carries the fork's published coreutils crate. +carried-coreutils = ["brush", "dep:brush-ocap-coreutils-builtins"] # Kernel-enforced L3 backstop (Linux). The engine applies the Landlock fs_write # ruleset before spawning when fs_write is restricted (agent-bridle#35); off this # build/OS it is the honest advisory None (ADR 0005 / ADR 0006). Exec/net axes @@ -53,10 +74,20 @@ serde_json = { workspace = true } async-trait = { workspace = true } tokio = { workspace = true, optional = true } -# brush deps — intentionally absent. `brush-bridle-core` (a renamed brush fork -# published to crates.io) is the DEFERRED, REVERSIBLE full-bash alternative -# engine behind the same registry seam (ADR 0005 D4); it would be added under -# its own optional feature when adopted, not restored here. See agent-bridle#20. +# The `brush-ocap-*` fork on crates.io (github.com/hartsock/brush @ brush-ocap): +# upstream brush + the CommandInterceptor hook (reubeno/brush#1184), renamed + +# published so this crate is crates.io-publishable with `brush` on. Temporary — +# retire once #1184 merges upstream and a hooked `brush-*` release ships +# (agent-bridle#20). +brush-ocap-core = { version = "0.5", optional = true } +brush-ocap-builtins = { version = "0.2", optional = true } +# A minimal carried coreutils set (ls/cat/echo) — enough for the embedded +# dispatch path (issue #206) without the ~100-crate `coreutils.all` build. +brush-ocap-coreutils-builtins = { version = "0.1", features = [ + "coreutils.ls", + "coreutils.cat", + "coreutils.echo", +], optional = true } [dev-dependencies] tokio = { workspace = true } diff --git a/agent-bridle-tool-shell/src/bin/dispatch_host.rs b/agent-bridle-tool-shell/src/bin/dispatch_host.rs new file mode 100644 index 0000000..39baf45 --- /dev/null +++ b/agent-bridle-tool-shell/src/bin/dispatch_host.rs @@ -0,0 +1,51 @@ +//! Dispatch-capable host binary for the carried-coreutils integration test +//! (issue #206). Its `main` calls [`maybe_dispatch`] first, so the brush engine's +//! carried coreutils shims — which re-exec ` --invoke-bundled ` — +//! resolve **in-process against THIS binary**. Run it with the environment +//! scrubbed (`env_clear`) to prove carried `ls`/`cat` work with no host tools. +//! +//! Usage: `dispatch_host [cwd]` — runs `` through the confined brush +//! engine under full-access, prints captured stdout/stderr, exits with the +//! command's code. + +use agent_bridle_core::{Caveats, Gate, Tool}; +use agent_bridle_tool_shell::{maybe_dispatch, BrushShellTool}; + +fn main() { + // MUST be first: if invoked as ` --invoke-bundled …`, run the + // carried coreutil in-process (a fresh post-exec process — safe) and exit. + if let Some(code) = maybe_dispatch() { + std::process::exit(code); + } + + let mut args = std::env::args().skip(1); + let cmd = args.next().expect("usage: dispatch_host [cwd]"); + let cwd = args.next(); + + let tool = BrushShellTool::new(); + let ctx = Gate::new(0) + .authorize(&tool, &Caveats::top()) + .expect("authorize"); + + let mut json = serde_json::json!({ "cmd": cmd }); + if let Some(cwd) = cwd { + json["cwd"] = serde_json::Value::String(cwd); + } + + let rt = tokio::runtime::Runtime::new().expect("tokio runtime"); + let out = rt + .block_on(async { tool.invoke(json, &ctx).await }) + .expect("invoke"); + + if let Some(s) = out.get("stdout").and_then(|v| v.as_str()) { + print!("{s}"); + } + if let Some(s) = out.get("stderr").and_then(|v| v.as_str()) { + eprint!("{s}"); + } + let code = out + .get("exit_code") + .and_then(serde_json::Value::as_i64) + .unwrap_or(1); + std::process::exit(code as i32); +} diff --git a/agent-bridle-tool-shell/src/brush_shell.rs b/agent-bridle-tool-shell/src/brush_shell.rs new file mode 100644 index 0000000..47d7ffc --- /dev/null +++ b/agent-bridle-tool-shell/src/brush_shell.rs @@ -0,0 +1,298 @@ +//! The carried **brush engine** (agent-bridle#20 / Track 2): a bash-in-Rust +//! shell run **in-process**, confined by the `CommandInterceptor` L2 leash. +//! +//! Unlike [`HostShellTool`](crate::HostShellTool) — which *refuses* a restricted +//! `exec`/`net` grant because it cannot bound a real `/bin/sh`'s forked children +//! — this engine's interceptor fires on every resolved program name and every +//! opened path *inside* brush (`before_exec` at the single external-spawn funnel, +//! `before_open` at `Shell::open_file`). So it **serves** restricted `exec`/`fs` +//! grants, in-process, on any platform, and records each denial into a shared +//! sink surfaced as structured `denials` on the envelope. +//! +//! It uses the temporary `brush-ocap-*` fork carrying the upstream hook PR +//! (reubeno/brush#1184). Enforcement is L2 (advisory `sandbox_kind = None`); an +//! L3 backstop is a follow-up. The curated builtin set removes `exec` (the one +//! builtin that spawns outside the `before_exec` funnel — a proven bypass). + +use std::collections::HashMap; +use std::io::Read; +use std::sync::{Arc, LazyLock, Mutex}; + +use agent_bridle_core::{ + default_exec_path, Denial, Disclosure, SandboxKind, Scope, Tool, ToolContext, ToolEnvelope, + ToolError, ToolResult, +}; +use async_trait::async_trait; +use brush_builtins::{default_builtins, BuiltinSet}; +use brush_core::builtins::Registration; +use brush_core::extensions::{DefaultErrorFormatter, ShellExtensionsImpl}; +use brush_core::openfiles::{OpenFile, OpenFiles}; +use brush_core::variables::ShellVariable; +use brush_core::{Shell, ShellFd}; + +use crate::caveat_interceptor::{CaveatInterceptor, DenialSink}; + +/// The engine identity stamped on the disclosure (ADR 0005 D2 / ADR 0019 D4). +const ENGINE_NAME: &str = "brush"; +/// Default cap on captured output bytes (mirrors the other engines' default). +const DEFAULT_MAX_OUTPUT: usize = 64 * 1024; +/// Minimal, standard `PATH` used when `exec` is RESTRICTED: external commands +/// must still *resolve* so they reach the interceptor's `before_exec` gate +/// (which then denies the out-of-scope ones). Under full-access the full ambient +/// path is used instead (see [`BrushShellTool::invoke`]). +const RESTRICTED_PATH: &str = "/usr/local/bin:/usr/bin:/bin"; +/// Builtins removed from the confined shell because they reach `spawn`/`exec` +/// *without* going through the interceptor funnels. The brush-fork audit found +/// exactly one: `exec` (its non-subshell branch calls `cmd.exec()` directly). A +/// confined shell needs nothing to replace into, so removing it closes a live +/// authority leak at zero cost. Every other spawn/open path funnels through a +/// hook and is intentionally kept. +const REMOVED_BUILTINS: &[&str] = &["exec"]; + +/// The brush [`ShellExtensions`](brush_core::extensions::ShellExtensions) carried +/// by this engine: the default error formatter plus the capability interceptor. +type LeashedExtensions = ShellExtensionsImpl; + +/// The engine's input schema (shared `cmd`/`env`/`cwd` contract with the other +/// engines), parsed once from the embedded data file — knowledge in data, not an +/// inline literal (three-Cs). +static DEFAULT_SCHEMA: LazyLock> = LazyLock::new(|| { + Arc::new( + serde_json::from_str(include_str!("host_shell.schema.json")) + .expect("embedded host_shell.schema.json must be valid JSON"), + ) +}); + +/// The carried brush engine — a [`Tool`] that runs a free-form command string +/// through an in-process bash-in-Rust shell confined by the `CommandInterceptor` +/// leash. Registered under `"shell"` (the ADR 0005 D2 seam), a peer of +/// [`ShellTool`](crate::ShellTool) / [`HostShellTool`](crate::HostShellTool). +#[derive(Clone)] +pub struct BrushShellTool { + max_output: usize, + schema: Arc, +} + +impl std::fmt::Debug for BrushShellTool { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BrushShellTool").finish_non_exhaustive() + } +} + +impl Default for BrushShellTool { + fn default() -> Self { + Self::new() + } +} + +impl BrushShellTool { + /// The engine with default output cap and the embedded schema. + #[must_use] + pub fn new() -> Self { + Self { + max_output: DEFAULT_MAX_OUTPUT, + schema: DEFAULT_SCHEMA.clone(), + } + } + + /// Override the tool's input schema (three-Cs: Configuration). + #[must_use] + pub fn with_schema(mut self, schema: serde_json::Value) -> Self { + self.schema = Arc::new(schema); + self + } + + fn disclosure(&self) -> Disclosure { + Disclosure { + engine: Some(ENGINE_NAME.to_string()), + ..Disclosure::default() + } + } +} + +#[async_trait] +impl Tool for BrushShellTool { + fn name(&self) -> &str { + "shell" + } + + fn schema(&self) -> serde_json::Value { + (*self.schema).clone() + } + + async fn invoke( + &self, + args: serde_json::Value, + cx: &ToolContext, + ) -> ToolResult { + let cmd = args + .get("cmd") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| ToolError::denied("brush: missing required `cmd` string"))? + .to_string(); + let cwd = args + .get("cwd") + .and_then(serde_json::Value::as_str) + .map(str::to_string); + + // PATH parity: the FULL ambient path when `exec` is unrestricted (so the + // agent's own tools — `~/.cargo/bin`, `/opt/homebrew/bin`, … — resolve + // like a host shell); a minimal standard path when `exec` is RESTRICTED, + // so externals still resolve to reach the `before_exec` gate that denies + // the out-of-scope ones. The host env is never inherited otherwise. + let path_value = if matches!(cx.caveats().exec, Scope::All) { + default_exec_path() + } else { + RESTRICTED_PATH.to_string() + }; + + let sink: DenialSink = Arc::new(Mutex::new(Vec::new())); + let interceptor = CaveatInterceptor::new(cx.clone(), Arc::clone(&sink)); + let max_output = self.max_output; + + // brush's shell API is async and blocks on a per-invocation current-thread + // runtime; run it off the async runtime on a blocking worker. + let captured = tokio::task::spawn_blocking(move || { + run_in_brush(cmd, cwd, path_value, interceptor, max_output) + }) + .await + .map_err(|e| ToolError::Exec(std::io::Error::other(format!("join: {e}"))))??; + + // Any denial the interceptor recorded lifts the envelope to `denied:true`. + let denials: Vec = sink.lock().map(|g| g.clone()).unwrap_or_default(); + + Ok(ToolEnvelope::new(SandboxKind::None) + .with_disclosure(self.disclosure()) + .with_exit_code(captured.exit_code) + .with_stdout(captured.stdout) + .with_stderr(captured.stderr) + .with_timed_out(false) + .with_denials(denials) + .into_json()) + } +} + +/// What a finished brush run produced. +struct Captured { + exit_code: i32, + stdout: String, + stderr: String, +} + +/// Drive a brush shell to completion for one command, capturing stdout/stderr +/// via real OS pipes (an `Arc>>` will not satisfy brush's fd +/// `Stream`; pipes are mandatory). The shell is built with the supplied +/// [`CaveatInterceptor`] so exec/open are confined in-process. IO must be enabled +/// on the runtime (not just time): `$(...)` sets up real pipes via tokio's IO +/// driver, and with IO enabled the inner program hits the `before_exec` funnel +/// (a legible recorded denial) rather than panicking. +fn run_in_brush( + cmd: String, + cwd: Option, + path_value: String, + interceptor: CaveatInterceptor, + max_output: usize, +) -> ToolResult { + let (out_reader, out_writer) = + std::io::pipe().map_err(|e| ToolError::Exec(brush_io("create stdout pipe", &e)))?; + let (err_reader, err_writer) = + std::io::pipe().map_err(|e| ToolError::Exec(brush_io("create stderr pipe", &e)))?; + + // Drain the read ends on background threads so a chatty command cannot + // deadlock by filling the pipe buffer before the shell exits. + let out_handle = std::thread::spawn(move || drain(out_reader, max_output)); + let err_handle = std::thread::spawn(move || drain(err_reader, max_output)); + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|e| ToolError::Exec(brush_io("build shell runtime", &e)))?; + + let working_dir = cwd.map(std::path::PathBuf::from); + + let exit_code = rt.block_on(async move { + let mut fds: HashMap = HashMap::new(); + fds.insert(OpenFiles::STDOUT_FD, OpenFile::from(out_writer)); + fds.insert(OpenFiles::STDERR_FD, OpenFile::from(err_writer)); + + let mut shell: Shell = + Shell::builder_with_extensions::() + .command_interceptor(interceptor) + .builtins(confined_builtins()) + .do_not_inherit_env(true) + .no_editing(true) + .interactive(false) + .fds(fds) + .maybe_working_dir(working_dir) + .build() + .await + .map_err(|e| ToolError::Exec(brush_io("build shell", &e)))?; + + // Register the carried coreutils shims (issue #206). They re-exec + // ` --invoke-bundled `, so they resolve in-process ONLY when + // the host binary is dispatch-capable (calls `maybe_dispatch()` in main). + // The re-exec still funnels through the `before_exec` interceptor. + #[cfg(feature = "carried-coreutils")] + { + crate::coreutils_dispatch::install_default_providers(); + crate::coreutils_dispatch::register_shims(&mut shell); + } + + shell + .env_mut() + .set_global("PATH", ShellVariable::new(path_value)) + .map_err(|e| ToolError::Exec(brush_io("seed PATH", &e)))?; + + let result = shell + .run_dash_c_command(cmd) + .await + .map_err(|e| ToolError::Exec(brush_io("run command", &e)))?; + + // Drop the shell so it releases its clones of the pipe writers; only then + // do the reader threads see EOF. + drop(shell); + + Ok::(i32::from(u8::from(result.exit_code))) + })?; + + let stdout = out_handle + .join() + .map_err(|_| ToolError::denied("stdout reader thread panicked"))??; + let stderr = err_handle + .join() + .map_err(|_| ToolError::denied("stderr reader thread panicked"))??; + + Ok(Captured { + exit_code, + stdout, + stderr, + }) +} + +/// The curated builtin set: the bash-mode default set with [`REMOVED_BUILTINS`] +/// stripped out (robust-by-construction — a removed builtin is simply gone, so a +/// confined shell running `exec` gets "command not found", never a spawn). +fn confined_builtins() -> HashMap> { + let mut builtins = default_builtins::(BuiltinSet::BashMode); + for name in REMOVED_BUILTINS { + builtins.remove(*name); + } + builtins +} + +/// Read a pipe to EOF (capped at `max` bytes), returning lossy UTF-8. +fn drain(mut reader: std::io::PipeReader, max: usize) -> ToolResult { + let mut buf = Vec::new(); + reader + .read_to_end(&mut buf) + .map_err(|e| ToolError::Exec(brush_io("drain pipe", &e)))?; + if buf.len() > max { + buf.truncate(max); + } + Ok(String::from_utf8_lossy(&buf).into_owned()) +} + +/// Wrap a brush/IO error with context as an [`std::io::Error`]. +fn brush_io(ctx: &str, e: &impl std::fmt::Display) -> std::io::Error { + std::io::Error::other(format!("{ctx}: {e}")) +} diff --git a/agent-bridle-tool-shell/src/caveat_interceptor.rs b/agent-bridle-tool-shell/src/caveat_interceptor.rs new file mode 100644 index 0000000..7d0da7b --- /dev/null +++ b/agent-bridle-tool-shell/src/caveat_interceptor.rs @@ -0,0 +1,324 @@ +//! [`CaveatInterceptor`] — the in-process capability hook for free-form shell. +//! +//! brush 0.5 cannot be confined in-process by a cleared `PATH` + a builtin +//! allow-list: any command whose name contains a path separator (e.g. +//! `/bin/rm`, `./payload`) bypasses both `PATH` and the builtin table and runs +//! directly (DESIGN §6). Our brush fork closes this by exposing +//! [`brush_core::extensions::CommandInterceptor`], whose `before_exec` / +//! `before_open` hooks fire at the **single external-spawn funnel** and at +//! `Shell::open_file` — so a policy applied here cannot be circumvented by +//! spelling a command (or a redirection target) differently. This makes the +//! confined shell a true superset of an `sh -c` cmd-string shell, cross-OS. +//! +//! [`CaveatInterceptor`] carries one invocation's **effective** caveats (the +//! `ToolContext` minted by the gate) and delegates every decision to the +//! *shared* leash logic on [`ToolContext`] — [`ToolContext::check_exec`], +//! [`ToolContext::check_path_read`], [`ToolContext::check_path_write`]. It does +//! **not** duplicate the canonicalizing path check; the one in +//! `agent-bridle-core` (realpath, reject symlink/`..` escapes) is the single +//! source of truth. + +use std::path::Path; +use std::sync::{Arc, Mutex}; + +use agent_bridle_core::{Denial, DenialKind, ToolContext}; +use brush_core::extensions::{CommandInterceptor, ExecDecision, OpenDecision}; + +/// The shared, per-invocation denial sink. +/// +/// brush clones the [`CaveatInterceptor`] internally (the trait requires +/// `Clone`), so to see *every* denial the shell hit we cannot store the log in +/// the struct by value — each clone would get its own copy. The `Arc>` +/// makes all clones write to one vec. The `ShellTool` creates a *fresh* sink per +/// `invoke`, so two concurrent invocations never share one — that is what keeps +/// denials from cross-contaminating across invocations. +pub(crate) type DenialSink = Arc>>; + +/// A brush [`CommandInterceptor`] that enforces an invocation's effective +/// caveats in-process **and records each denial it makes** into a shared sink. +/// +/// Holds an `Option`: +/// +/// - `Some(cx)` — enforce `cx`'s effective caveats (the normal, constructed +/// case). Built per-invocation via [`CaveatInterceptor::new`]. +/// - `None` — the [`Default`] value. **Conservatively denies everything.** The +/// trait requires `Default`, but a `ToolContext` is un-forgeable (no public +/// constructor by design), so the default cannot carry caveats; denying is the +/// only safe behavior. A default-constructed interceptor must never reach a +/// live shell — but if one ever did, it is fail-closed, not allow-all. +/// +/// Every `Deny` is appended to the [`DenialSink`] so the shell tool can read a +/// **structured** signal after the run instead of string-matching stderr. An +/// `Allow` records nothing — so a permitted command that exits non-zero on its +/// own (e.g. exit 126) is never mistaken for a leash denial. +#[derive(Debug, Clone, Default)] +pub struct CaveatInterceptor { + /// The minted context whose effective caveats gate this shell, or `None` + /// for the fail-closed default. + cx: Option, + /// Shared sink every denial is recorded into. `None` only for the + /// [`Default`] interceptor (which still denies, just records nothing — + /// it never reaches a live shell). + sink: Option, +} + +impl CaveatInterceptor { + /// Build an interceptor that enforces `cx`'s effective caveats and records + /// every denial it makes into `sink`. + #[must_use] + pub(crate) fn new(cx: ToolContext, sink: DenialSink) -> Self { + Self { + cx: Some(cx), + sink: Some(sink), + } + } + + /// Record a denial into the shared sink (a no-op if there is no sink). + fn record(&self, kind: DenialKind, target: impl Into, reason: impl Into) { + if let Some(sink) = &self.sink { + // A poisoned mutex would only happen if a brush callback panicked + // mid-record; recover the inner vec rather than poison-propagate so + // a single bad record cannot lose the rest of the denial log. + let mut guard = sink + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + guard.push(Denial { + kind, + target: target.into(), + reason: reason.into(), + }); + } + } +} + +impl CommandInterceptor for CaveatInterceptor { + /// Deny unless the effective `exec` caveat allows `program`. + /// + /// `program` is what brush is about to spawn: for `PATH`-resolved commands + /// the resolved absolute path, and for path-separator commands the path as + /// written (`/bin/rm`, `./x`). We hand that string to + /// [`ToolContext::check_exec`], which allows it if the `exec` scope contains + /// it verbatim OR contains its basename — so a bare-name grant (`["git"]`) + /// matches the resolved `/usr/bin/git`, while `/bin/rm` is denied whenever + /// neither `rm` nor `/bin/rm` is granted (the path-separator bypass stays + /// closed at the funnel). + fn before_exec(&self, program: &str, _args: &[String]) -> ExecDecision { + match &self.cx { + Some(cx) => match cx.check_exec(program) { + Ok(()) => ExecDecision::Allow, + Err(e) => { + let reason = e.to_string(); + // Record the denial as a structured signal BEFORE returning. + self.record(DenialKind::Exec, program, &reason); + ExecDecision::Deny(reason) + } + }, + // Fail-closed default: no caveats means no authority. + None => { + let reason = "no effective caveats (default interceptor); exec denied".to_string(); + self.record(DenialKind::Exec, program, &reason); + ExecDecision::Deny(reason) + } + } + } + + /// Deny unless the effective `fs_read`/`fs_write` caveat allows `path`. + /// + /// `write` selects the axis. Both checks canonicalize first (realpath) and + /// reject paths that escape the granted scope via `..` or a symlink — that + /// logic is the shared one in `agent-bridle-core`, reused here, not copied. + fn before_open(&self, path: &Path, write: bool) -> OpenDecision { + let Some(cx) = &self.cx else { + let reason = "no effective caveats (default interceptor); open denied".to_string(); + self.record(DenialKind::Open, path.to_string_lossy(), &reason); + return OpenDecision::Deny(reason); + }; + let result = if write { + cx.check_path_write(path) + } else { + cx.check_path_read(path) + }; + match result { + Ok(()) => OpenDecision::Allow, + Err(e) => { + let reason = e.to_string(); + self.record(DenialKind::Open, path.to_string_lossy(), &reason); + OpenDecision::Deny(reason) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use agent_bridle_core::{Caveats, Gate, Scope, Tool, ToolResult}; + + /// Mint a `ToolContext` the only legitimate way — through the gate — using a + /// trivial tool with default (`top`) requirements so `effective == granted`. + fn ctx(granted: Caveats) -> ToolContext { + struct AnyTool; + #[async_trait::async_trait] + impl Tool for AnyTool { + fn name(&self) -> &str { + "any" + } + fn schema(&self) -> serde_json::Value { + serde_json::json!({}) + } + async fn invoke( + &self, + _args: serde_json::Value, + _cx: &ToolContext, + ) -> ToolResult { + Ok(serde_json::Value::Null) + } + } + Gate::new(0) + .authorize(&AnyTool, &granted) + .expect("authorize") + } + + /// Build an interceptor with a fresh sink, returning both so a test can + /// assert on what was recorded. + fn interceptor_with_sink(granted: Caveats) -> (CaveatInterceptor, DenialSink) { + let sink: DenialSink = Arc::new(Mutex::new(Vec::new())); + let interceptor = CaveatInterceptor::new(ctx(granted), Arc::clone(&sink)); + (interceptor, sink) + } + + /// Snapshot the sink's recorded denials. + fn drain(sink: &DenialSink) -> Vec { + sink.lock().unwrap().clone() + } + + #[test] + fn default_is_fail_closed() { + let interceptor = CaveatInterceptor::default(); + assert!(matches!( + interceptor.before_exec("echo", &[]), + ExecDecision::Deny(_) + )); + assert!(matches!( + interceptor.before_open(Path::new("/tmp"), false), + OpenDecision::Deny(_) + )); + } + + #[test] + fn before_exec_allows_in_scope_denies_out_of_scope() { + let (interceptor, sink) = interceptor_with_sink(Caveats { + exec: Scope::only(["echo".to_string()]), + ..Caveats::top() + }); + assert!(matches!( + interceptor.before_exec("echo", &[]), + ExecDecision::Allow + )); + assert!(matches!( + interceptor.before_exec("rm", &["-rf".to_string()]), + ExecDecision::Deny(_) + )); + // An Allow records nothing; only the Deny is in the sink. + let recorded = drain(&sink); + assert_eq!( + recorded.len(), + 1, + "exactly one denial expected: {recorded:?}" + ); + assert_eq!(recorded[0].kind, DenialKind::Exec); + assert_eq!(recorded[0].target, "rm"); + assert!(recorded[0].reason.contains("not within the granted")); + } + + #[test] + fn before_exec_denies_path_separator_spelled_command() { + // The load-bearing case the hook exists for: `/bin/rm` is denied because + // `/bin/rm` is not within `exec: Only{echo}` — the path-separator bypass + // is closed, since the hook fires even for path-separator commands. + let (interceptor, sink) = interceptor_with_sink(Caveats { + exec: Scope::only(["echo".to_string()]), + ..Caveats::top() + }); + assert!(matches!( + interceptor.before_exec("/bin/rm", &["-rf".to_string()]), + ExecDecision::Deny(_) + )); + // The denial records the path-separator program verbatim. + let recorded = drain(&sink); + assert_eq!(recorded.len(), 1); + assert_eq!(recorded[0].kind, DenialKind::Exec); + assert_eq!(recorded[0].target, "/bin/rm"); + } + + #[test] + fn allow_records_nothing_in_sink() { + // A permitted exec must NOT leave a denial — this is what keeps a + // permitted command that exits 126 on its own from being flagged. + let (interceptor, sink) = interceptor_with_sink(Caveats { + exec: Scope::only(["echo".to_string()]), + ..Caveats::top() + }); + assert!(matches!( + interceptor.before_exec("echo", &[]), + ExecDecision::Allow + )); + assert!(drain(&sink).is_empty(), "an Allow must record nothing"); + } + + #[test] + fn before_open_write_uses_fs_write_axis() { + let dir = std::env::temp_dir(); + let (interceptor, _sink) = interceptor_with_sink(Caveats { + fs_write: Scope::only([dir.to_string_lossy().into_owned()]), + ..Caveats::top() + }); + // A new file under the allowed dir: write allowed. + assert!(matches!( + interceptor.before_open(&dir.join("ab-interceptor-ok.txt"), true), + OpenDecision::Allow + )); + // Clearly outside the allowed dir: write denied. + assert!(matches!( + interceptor.before_open(Path::new("/etc/shadow"), true), + OpenDecision::Deny(_) + )); + } + + #[test] + fn before_open_denial_is_recorded_as_open_kind() { + let dir = std::env::temp_dir(); + let (interceptor, sink) = interceptor_with_sink(Caveats { + fs_write: Scope::only([dir.to_string_lossy().into_owned()]), + ..Caveats::top() + }); + let _ = interceptor.before_open(Path::new("/etc/shadow"), true); + let recorded = drain(&sink); + assert_eq!(recorded.len(), 1, "one open denial expected: {recorded:?}"); + assert_eq!(recorded[0].kind, DenialKind::Open); + assert_eq!(recorded[0].target, "/etc/shadow"); + } + + #[test] + fn clones_share_one_sink() { + // brush clones the interceptor; every clone must write to the same sink + // (that is why the sink is Arc-shared). Two denials via two clones + // appear in the one shared log. + let (interceptor, sink) = interceptor_with_sink(Caveats { + exec: Scope::only(["echo".to_string()]), + ..Caveats::top() + }); + let clone = interceptor.clone(); + let _ = interceptor.before_exec("rm", &[]); + let _ = clone.before_exec("curl", &[]); + let recorded = drain(&sink); + assert_eq!( + recorded.len(), + 2, + "both clones' denials must land: {recorded:?}" + ); + let targets: Vec<&str> = recorded.iter().map(|d| d.target.as_str()).collect(); + assert!(targets.contains(&"rm")); + assert!(targets.contains(&"curl")); + } +} diff --git a/agent-bridle-tool-shell/src/coreutils_dispatch.rs b/agent-bridle-tool-shell/src/coreutils_dispatch.rs new file mode 100644 index 0000000..b295a6b --- /dev/null +++ b/agent-bridle-tool-shell/src/coreutils_dispatch.rs @@ -0,0 +1,206 @@ +//! Carried-coreutils dispatch **as a library** (Track 2 Gate 2, agent-bridle#20 +//! / issue #206). +//! +//! Brush's bundled `uu_*` coreutils run busybox-style: a shim builtin re-executes +//! the current process as `current_exe() --invoke-bundled `, and a +//! dispatcher at the top of the binary's `main` turns that back into a call of +//! the `uu_*::uumain`. That dispatcher normally lives in the `brush-shell` +//! *binary*, so an **embedder** (whose `current_exe()` is the host binary, e.g. +//! `newt`) can't use the carried coreutils. +//! +//! This module lifts the dispatcher + shim registration into a library so any +//! embedder can make its own binary dispatch-capable with one line in `main`: +//! +//! ```no_run +//! if let Some(code) = agent_bridle_tool_shell::maybe_dispatch() { +//! std::process::exit(code); +//! } +//! ``` +//! +//! We keep the **re-exec (fork+exec)** model on purpose. "fork and call `uumain` +//! in-process" is unsafe here: agent-bridle runs under a multithreaded runtime, +//! and `fork()` without a following `exec` leaves other threads' locks +//! (allocator, …) held forever in the child — `uumain` allocates on its first +//! line and would deadlock. `execve` resets the address space, so re-exec is +//! safe *and* portable (works on Windows too). +//! +//! Cribbed from our brush fork's `brush-shell/src/bundled.rs` (same license); to +//! be upstreamed to reubeno/brush as the dispatch-as-a-library follow-up to the +//! CommandInterceptor hook (reubeno/brush#1184). + +use std::collections::HashMap; +use std::ffi::OsString; +use std::io::Write; +use std::path::PathBuf; +use std::sync::OnceLock; + +use brush_core::builtins::{BoxFuture, ContentOptions, ContentType, Registration}; +use brush_core::commands::{self, CommandArg, ExecutionContext}; +use brush_core::extensions::ShellExtensions; +use brush_core::ExecutionExitCode; + +/// The leading flag that signals a bundled-command dispatch. Deliberately +/// obscure so it does not collide with real flags or script tokens. +pub const DISPATCH_FLAG: &str = "--invoke-bundled"; + +/// Signature of a bundled command's entry point — matches `uu_*::uumain`. +pub type BundledFn = fn(args: Vec) -> i32; + +/// Process-wide registry. Set once, read on each shim invocation + dispatch. +static REGISTRY: OnceLock> = OnceLock::new(); + +/// Cached path to the running executable. +static SELF_EXE: OnceLock> = OnceLock::new(); + +/// Install the bundled-command registry. Idempotent (first call wins). +#[allow( + clippy::implicit_hasher, + reason = "registry uses the default hasher; callers build with HashMap::new()" +)] +pub fn install(commands: HashMap) { + let _ = REGISTRY.set(commands); +} + +/// Install the registry from all compiled-in providers. Call once, before +/// [`maybe_dispatch`] / [`register_shims`], so both the dispatch fast-path and +/// the shell's shim builtins see a populated registry. +pub fn install_default_providers() { + let mut commands: HashMap = HashMap::new(); + commands.extend(brush_coreutils_builtins::bundled_commands()); + install(commands); +} + +/// Run the bundled-command fast path if the process was invoked for it. +/// +/// If invoked as ` --invoke-bundled [ARGS…]`, runs the registered +/// function and returns `Some(code)`; the caller exits with it. Returns `None` +/// for a normal invocation, so the embedder's usual startup proceeds. If the +/// registry was never installed (a non-dispatch-capable build), we install the +/// default providers on demand so a dispatch-capable `main` need only call this. +#[must_use] +pub fn maybe_dispatch() -> Option { + let mut raw = std::env::args_os(); + let _argv0 = raw.next(); + let first = raw.next()?; + if first != DISPATCH_FLAG { + return None; + } + + if REGISTRY.get().is_none() { + install_default_providers(); + } + + let rest: Vec = raw.collect(); + let Some((name, args)) = rest.split_first() else { + eprintln!("carried-coreutils: {DISPATCH_FLAG} requires a command name"); + return Some(u8::from(ExecutionExitCode::InvalidUsage).into()); + }; + let Some(name_str) = name.to_str() else { + eprintln!( + "carried-coreutils: unknown bundled command: {}", + name.to_string_lossy() + ); + return Some(u8::from(ExecutionExitCode::NotFound).into()); + }; + let Some(func) = REGISTRY.get().and_then(|r| r.get(name_str)) else { + eprintln!("carried-coreutils: unknown bundled command: {name_str}"); + return Some(u8::from(ExecutionExitCode::NotFound).into()); + }; + + let mut argv: Vec = Vec::with_capacity(1 + args.len()); + argv.push(name.clone()); + argv.extend(args.iter().cloned()); + Some(func(argv)) +} + +/// Path to the running executable (cached). +fn self_exe() -> Option<&'static PathBuf> { + SELF_EXE + .get_or_init(|| std::env::current_exe().ok()) + .as_ref() +} + +/// Help/usage content provider for the shim builtin. +#[allow( + clippy::needless_pass_by_value, + clippy::unnecessary_wraps, + reason = "signature dictated by brush_core::builtins::CommandContentFunc" +)] +fn shim_content( + name: &str, + content_type: ContentType, + _options: &ContentOptions, +) -> Result { + match content_type { + ContentType::ShortDescription => Ok(format!("{name} - carried bundled command")), + ContentType::DetailedHelp => Ok(format!( + "{name} - carried bundled command (runs via ` {DISPATCH_FLAG} {name}`)\n" + )), + ContentType::ShortUsage | ContentType::ManPage => Ok(String::new()), + } +} + +/// Builtin execute function shared by all shims: re-exec the running executable +/// as ` --invoke-bundled `. The path separator in the exe +/// path routes `SimpleCommand::execute` straight to the external-exec path +/// (which funnels through the `before_exec` interceptor — the leash still holds) +/// and inherits the shell's redirection state. +fn shim_execute( + context: ExecutionContext<'_, SE>, + args: Vec, +) -> BoxFuture<'_, Result> { + Box::pin(async move { + let exe_path = if let Some(p) = self_exe() { + p.to_string_lossy().into_owned() + } else { + let _ = writeln!( + context.stderr(), + "carried-coreutils: cannot determine path to running executable" + ); + return Ok(ExecutionExitCode::CannotExecute.into()); + }; + + let bundled_name = context.command_name.clone(); + let mut child_args: Vec = Vec::with_capacity(args.len() + 2); + child_args.push(CommandArg::String(String::new())); // argv[0], dropped + child_args.push(CommandArg::String(DISPATCH_FLAG.into())); + child_args.push(CommandArg::String(bundled_name.clone())); + child_args.extend(args.into_iter().skip(1)); + + let mut cmd = commands::SimpleCommand::new( + commands::ShellForCommand::ParentShell(context.shell), + context.params, + exe_path, + child_args, + ); + cmd.use_functions = false; + cmd.argv0 = Some(bundled_name); + + let spawn_result = cmd.execute().await?; + let wait_result = spawn_result.wait().await?; + Ok(wait_result.into()) + }) +} + +/// A [`Registration`] for the shim builtin (reused for every bundled name). +fn shim_registration() -> Registration { + Registration { + execute_func: shim_execute::, + content_func: shim_content, + disabled: false, + special_builtin: false, + declaration_builtin: false, + } +} + +/// Register a shim builtin for every name in the installed registry, using +/// `register_builtin_if_unset` so brush's own builtins win on conflict. Call +/// [`install_default_providers`] first. +pub fn register_shims(shell: &mut brush_core::Shell) { + let Some(registry) = REGISTRY.get() else { + return; + }; + for name in registry.keys() { + shell.register_builtin_if_unset(name.clone(), shim_registration::()); + } +} diff --git a/agent-bridle-tool-shell/src/lib.rs b/agent-bridle-tool-shell/src/lib.rs index a5cc6be..74759c8 100644 --- a/agent-bridle-tool-shell/src/lib.rs +++ b/agent-bridle-tool-shell/src/lib.rs @@ -31,6 +31,12 @@ #![forbid(unsafe_code)] #![warn(missing_docs)] +#[cfg(feature = "brush")] +mod brush_shell; +#[cfg(feature = "brush")] +mod caveat_interceptor; +#[cfg(feature = "carried-coreutils")] +mod coreutils_dispatch; #[cfg(feature = "host-shell")] mod host_shell; #[cfg(feature = "shell")] @@ -49,6 +55,24 @@ pub use shell_tool::ShellTool; #[cfg(feature = "host-shell")] pub use host_shell::HostShellTool; +/// The carried **brush** engine (agent-bridle#20 / Track 2): a bash-in-Rust +/// shell run in-process, confined by the `CommandInterceptor` L2 leash — the +/// only engine that also confines a *restricted* `exec`/`net` grant, on any +/// platform. Opt-in via the `brush` feature; a construction-time alternative to +/// [`ShellTool`] behind the ADR 0005 D2 seam, using the temporary `brush-ocap-*` +/// fork (reubeno/brush#1184). +#[cfg(feature = "brush")] +pub use brush_shell::BrushShellTool; + +/// Carried-coreutils dispatch (agent-bridle#20 / issue #206). An embedder's +/// binary calls [`maybe_dispatch`] at the top of `main` to become +/// dispatch-capable, so the brush engine's carried `ls`/`cat`/… shims (which +/// re-exec ` --invoke-bundled `) resolve in-process against the host +/// binary — carried coreutils with no host tools. [`register_shims`] / +/// [`install_default_providers`] are used by the engine internally. +#[cfg(feature = "carried-coreutils")] +pub use coreutils_dispatch::{install_default_providers, maybe_dispatch, register_shims}; + /// Network egress audit surface (#124, ADR 0016): the loopback proxy records /// every proxy-visible connection as a [`NetAuditEvent`] through an [`AuditSink`] /// (default off; enable via the `BRIDLE_NET_AUDIT` setting). The `bridle-netmon` diff --git a/agent-bridle-tool-shell/tests/brush_real.rs b/agent-bridle-tool-shell/tests/brush_real.rs new file mode 100644 index 0000000..acf7d0f --- /dev/null +++ b/agent-bridle-tool-shell/tests/brush_real.rs @@ -0,0 +1,109 @@ +//! Real-spawn reality-check for the carried **brush engine** (agent-bridle#20). +//! +//! Proves the engine's whole thesis: it runs a **dynamic construct the +//! safe-subset engine refuses** (`$(...)`) in-process, AND — unlike the +//! sandbox-host engine, which refuses a restricted `exec` grant — it **confines** +//! a restricted `exec` grant in-process via the `CommandInterceptor`, denying an +//! out-of-scope command (structured `denied:true`) while it never runs. +//! +//! Kept out of the unit tests (which mock the spawner) per the workspace norm. +#![cfg(feature = "brush")] + +use std::sync::atomic::{AtomicU64, Ordering}; + +use agent_bridle_core::{Caveats, Gate, Scope, Tool, ToolContext}; +use agent_bridle_tool_shell::BrushShellTool; + +/// Mint a [`ToolContext`] carrying `granted` — the public-API path an embedder +/// uses (mirrors `host_shell_real.rs`). +fn ctx(granted: Caveats) -> ToolContext { + Gate::new(0) + .authorize(&BrushShellTool::new(), &granted) + .expect("authorize") +} + +fn unique_temp(tag: &str) -> std::path::PathBuf { + static N: AtomicU64 = AtomicU64::new(0); + std::env::temp_dir().join(format!( + "ab-brush-{}-{}-{}", + tag, + std::process::id(), + N.fetch_add(1, Ordering::Relaxed) + )) +} + +/// Full-access: a `$(...)` command substitution — refused by the safe-subset +/// engine — RUNS in-process, and the engine identity is disclosed. +#[tokio::test] +async fn full_access_runs_dynamic_construct_and_captures() { + let out = BrushShellTool::new() + .invoke( + serde_json::json!({ "cmd": "echo \"$(echo composed)\"" }), + &ctx(Caveats::top()), + ) + .await + .expect("invoke"); + + assert_ne!(out["denied"], true, "ambient grant must run: {out}"); + assert_eq!(out["exit_code"], 0, "the command must succeed: {out}"); + assert_eq!( + out["stdout"].as_str().unwrap_or("").trim(), + "composed", + "the $(...) substitution must have executed in-process: {out}" + ); + assert_eq!( + out["disclosure"]["engine"], "brush", + "engine identity must be disclosed: {out}" + ); +} + +/// Restricted `exec` (only `echo`): an out-of-scope external is DENIED by the +/// interceptor — structured `denied:true`, `kind:"exec"` — and never runs. This +/// is the engine's differentiator: it confines a restricted exec grant that the +/// sandbox-host engine refuses to serve. +#[tokio::test] +async fn restricted_exec_denies_out_of_scope_command_in_process() { + let caveats = Caveats { + exec: Scope::only(["echo".to_string()]), + ..Caveats::top() + }; + let sentinel = unique_temp("exec-sentinel"); + let _ = std::fs::remove_file(&sentinel); + // Path-separator form goes straight to the external-spawn funnel → before_exec. + let cmd = format!("/bin/touch {}", sentinel.to_string_lossy()); + + let out = BrushShellTool::new() + .invoke(serde_json::json!({ "cmd": cmd }), &ctx(caveats)) + .await + .expect("invoke"); + + assert_eq!( + out["denied"], true, + "an out-of-scope exec must be denied by the interceptor: {out}" + ); + assert_eq!( + out["denials"][0]["kind"], "exec", + "denial names exec: {out}" + ); + assert!( + !sentinel.exists(), + "the denied command must not have run: {out}" + ); +} + +/// Restricted `exec` (only `echo`): an in-scope command still runs. +#[tokio::test] +async fn restricted_exec_allows_in_scope_command() { + let caveats = Caveats { + exec: Scope::only(["echo".to_string()]), + ..Caveats::top() + }; + + let out = BrushShellTool::new() + .invoke(serde_json::json!({ "cmd": "echo ok" }), &ctx(caveats)) + .await + .expect("invoke"); + + assert_ne!(out["denied"], true, "in-scope command must run: {out}"); + assert_eq!(out["stdout"].as_str().unwrap_or("").trim(), "ok", "{out}"); +} diff --git a/agent-bridle-tool-shell/tests/carried_coreutils.rs b/agent-bridle-tool-shell/tests/carried_coreutils.rs new file mode 100644 index 0000000..9b03e27 --- /dev/null +++ b/agent-bridle-tool-shell/tests/carried_coreutils.rs @@ -0,0 +1,81 @@ +//! Integration keystone for carried coreutils (Track 2 Gate 2 / issue #206). +//! +//! Runs the dispatch-capable `dispatch_host` binary with the **environment +//! scrubbed** (`env_clear` — no `PATH`, no host tools), asking the embedded +//! brush engine to run `ls`/`cat`. These succeed ONLY if the carried uutils +//! coreutils dispatch in-process via re-exec of the (dispatch-capable) host +//! binary — proving the "just a filesystem" story. If the dispatch machinery +//! regressed, the shell would find no `ls`/`cat` at all. +#![cfg(feature = "carried-coreutils")] + +use std::path::PathBuf; +use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; + +/// The dispatch-capable helper binary cargo built for us. +fn dispatch_host() -> &'static str { + env!("CARGO_BIN_EXE_dispatch_host") +} + +fn unique_temp(tag: &str) -> PathBuf { + static N: AtomicU64 = AtomicU64::new(0); + std::env::temp_dir().join(format!( + "ab-carried-{}-{}-{}", + tag, + std::process::id(), + N.fetch_add(1, Ordering::Relaxed) + )) +} + +/// Carried `ls` lists a directory with the environment fully scrubbed — no host +/// `/bin/ls`, no `PATH`. It resolves to the in-process uutils `ls` via the shim's +/// re-exec of the dispatch-capable host binary. +#[test] +fn carried_ls_runs_in_process_with_env_scrubbed() { + let dir = unique_temp("ls"); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write(dir.join("MARKER.txt"), b"x").unwrap(); + + let out = Command::new(dispatch_host()) + .env_clear() + .arg(format!("ls {}", dir.to_string_lossy())) + .output() + .expect("run dispatch_host"); + + let stdout = String::from_utf8_lossy(&out.stdout); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + out.status.success(), + "carried ls exited nonzero: stdout={stdout:?} stderr={stderr:?}" + ); + assert!( + stdout.contains("MARKER.txt"), + "carried ls must list the dir with NO host tools: stdout={stdout:?} stderr={stderr:?}" + ); + + let _ = std::fs::remove_dir_all(&dir); +} + +/// Carried `cat` reads a file with the environment fully scrubbed. +#[test] +fn carried_cat_runs_in_process_with_env_scrubbed() { + let dir = unique_temp("cat"); + std::fs::create_dir_all(&dir).unwrap(); + let file = dir.join("hello.txt"); + std::fs::write(&file, b"carried-cat-ok\n").unwrap(); + + let out = Command::new(dispatch_host()) + .env_clear() + .arg(format!("cat {}", file.to_string_lossy())) + .output() + .expect("run dispatch_host"); + + let stdout = String::from_utf8_lossy(&out.stdout); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + stdout.contains("carried-cat-ok"), + "carried cat must read the file with NO host tools: stdout={stdout:?} stderr={stderr:?}" + ); + + let _ = std::fs::remove_dir_all(&dir); +} diff --git a/agent-bridle/Cargo.toml b/agent-bridle/Cargo.toml index 53d28f2..62a40e8 100644 --- a/agent-bridle/Cargo.toml +++ b/agent-bridle/Cargo.toml @@ -19,6 +19,16 @@ shell = ["dep:agent-bridle-tool-shell"] # construction; it is NOT auto-added to `registry()` (it and `ShellTool` share # the `"shell"` name). Off by default. host-shell = ["dep:agent-bridle-tool-shell", "agent-bridle-tool-shell/host-shell"] +# The carried brush engine (agent-bridle#20 / Track 2): exposes `BrushShellTool`, +# a bash-in-Rust shell confined in-process by the CommandInterceptor — the only +# engine that also confines a restricted exec/net grant. Complementary to `shell` +# (ADR 0005 D2 seam); the embedder selects it at registry construction. Off by +# default. Uses the temporary `brush-ocap-*` fork published to crates.io +# (agent-bridle#20); retire once reubeno/brush#1184 ships upstream. +brush = ["dep:agent-bridle-tool-shell", "agent-bridle-tool-shell/brush"] +# Carried coreutils (issue #206): the brush engine carries bundled uu_* coreutils +# so an embedded shell runs `ls`/`cat`/… with no host tools. Off by default. +carried-coreutils = ["brush", "agent-bridle-tool-shell/carried-coreutils"] # `web` adds the confined `web_fetch` tool (the `net` enforcer). Off by default # so the heavy reqwest/readability/DNS deps only enter a build that asks for it. web = ["dep:agent-bridle-tool-web"] diff --git a/agent-bridle/src/lib.rs b/agent-bridle/src/lib.rs index a64a34d..09a7ef4 100644 --- a/agent-bridle/src/lib.rs +++ b/agent-bridle/src/lib.rs @@ -42,6 +42,13 @@ pub use agent_bridle_tool_shell::ShellTool; // `"shell"` name with `ShellTool` (ADR 0019 D3). #[cfg(feature = "host-shell")] pub use agent_bridle_tool_shell::HostShellTool; +// The carried brush engine (agent-bridle#20): a bash-in-Rust shell confined +// in-process by the CommandInterceptor — the only engine that also confines a +// restricted exec/net grant. Behind the `brush` feature (the crates.io +// `brush-ocap-*` fork); NOT auto-added to `registry()` — it shares the +// `"shell"` name with ShellTool (ADR 0005 D2), so the embedder selects it. +#[cfg(feature = "brush")] +pub use agent_bridle_tool_shell::BrushShellTool; #[cfg(feature = "web")] pub use agent_bridle_tool_web::WebFetchTool;