diff --git a/CHANGELOG.md b/CHANGELOG.md index 39fb87f..655c415 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## [0.3.0] - 2026-06-01 + +### Added + +- **System identity (`system.md`)** — `~/.openheim/system.md` defines the agent's base identity and is injected into the system prompt on every session. `openheim init` creates a default file. The prompt is now structured: identity block first, then skills, separated by clear section headers. +- **`default_skills` in config** — new `default_skills` array in `config.toml` auto-loads a set of skills into every session without passing `--skills` each time. Per-session skills are merged on top; duplicates are removed with defaults appearing first. +- **`default_skills` in `OpenheimBuilder`** — `.default_skills(vec![...])` builder method brings the same control to programmatic embeddings. +- **Work-directory sandbox** — new `work_dir` field in `config.toml` restricts `read_file` and `write_file` to a directory tree. When unset, the directory from which openheim is invoked is used. Symlinks are followed and canonicalized so they cannot be used to escape the boundary. +- **Shell access control** — new `allow_shell` boolean in `config.toml` (default `true`). When `false`, the `execute_command` tool is removed from the tool list entirely — the LLM never sees it and cannot request it. +- **Builder methods for security controls** — `OpenheimClient::builder()` gains `.work_dir(path)` and `.allow_shell(bool)`. Both override the corresponding config-file values. +- **Cross-compilation config** — `Cross.toml` added for building Linux targets from macOS. + +### Fixed + +- **MCP subprocess stderr suppression** — stderr from spawned MCP server processes no longer leaks into the terminal. +- **`run` command exits cleanly** — the process now exits after a headless `openheim run` prompt completes instead of hanging. +- **`merge_skills` deduplication** — skills within the `default_skills` list itself are now deduplicated, not just across the default/session boundary. +- **Whitespace preserved in system identity** — leading and trailing whitespace in `system.md` content is preserved when building the system prompt. +- **Accurate `init` error message** — `openheim init` now correctly reports whether `system.md` was created when the config already exists. + +### Breaking changes (library) + +- `AppConfig` gained two new public fields: `work_dir: Option` and `allow_shell: bool`. Code constructing `AppConfig` via struct literal (rather than TOML or the builder) must now supply these fields. Both have serde defaults so TOML loading is unaffected. +- `SystemToolExecutor::build` takes an additional `allow_shell: bool` argument. + ## [0.2.1] - 2026-05-28 ### Added diff --git a/Cargo.lock b/Cargo.lock index 1a08833..31a3a36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,11 +28,10 @@ dependencies = [ [[package]] name = "agent-client-protocol-derive" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce42c2d3c048c12897eef2e577dfff1e3355c632c9f1625cc953b9df48b44631" +checksum = "cabdc9d845d08ec7ed2d0c9de1ae4a1b198301407d55855261572761be90ec9f" dependencies = [ - "proc-macro2", "quote", "syn", ] @@ -94,9 +93,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -109,15 +108,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -167,9 +166,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "axum" @@ -240,9 +239,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -253,17 +252,26 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cassowary" @@ -282,9 +290,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.52" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "shlex", @@ -310,9 +318,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -324,9 +332,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.54" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -334,9 +342,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -346,9 +354,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -358,9 +366,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "clipboard-win" @@ -373,9 +381,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" @@ -389,9 +397,9 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +checksum = "7fd622ebbb56a5b2ccb651b32b911cdeb2a9b4b11776b2473bf26a26a286244e" dependencies = [ "castaway", "cfg-if", @@ -420,6 +428,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -456,10 +474,10 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "crossterm_winapi", "futures-core", - "mio 1.1.1", + "mio 1.2.1", "parking_lot", "rustix 0.38.44", "signal-hook", @@ -486,38 +504,14 @@ dependencies = [ "typenum", ] -[[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", + "darling_core", + "darling_macro", ] [[package]] @@ -533,24 +527,13 @@ dependencies = [ "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", + "darling_core", "quote", "syn", ] @@ -563,9 +546,9 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", "serde_core", @@ -627,9 +610,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -687,9 +670,9 @@ checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fd-lock" @@ -698,27 +681,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.59.0", ] [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ "cfg-if", "libc", - "libredox", - "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" @@ -914,15 +895,28 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + [[package]] name = "h2" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", @@ -930,7 +924,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.13.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -956,9 +950,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -983,9 +977,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -1028,9 +1022,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.8.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -1043,7 +1037,6 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1051,15 +1044,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -1083,14 +1075,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1133,12 +1124,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1146,9 +1138,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1159,9 +1151,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1173,15 +1165,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1193,15 +1185,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1212,6 +1204,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -1231,9 +1229,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1252,12 +1250,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -1293,11 +1291,11 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.10" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6778b0196eefee7df739db78758e5cf9b37412268bfa5650bfeed028aed20d9c" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" dependencies = [ - "darling 0.20.11", + "darling", "indoc", "proc-macro2", "quote", @@ -1306,19 +1304,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "is_terminal_polyfill" @@ -1337,15 +1325,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.97" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ "cfg-if", "futures-util", @@ -1365,9 +1353,9 @@ dependencies = [ [[package]] name = "kqueue" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" dependencies = [ "kqueue-sys", "libc", @@ -1375,11 +1363,11 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.4" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.1", "libc", ] @@ -1389,6 +1377,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.186" @@ -1397,13 +1391,11 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ - "bitflags 2.10.0", "libc", - "redox_syscall 0.7.0", ] [[package]] @@ -1414,15 +1406,15 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -1435,9 +1427,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "lru" @@ -1465,9 +1457,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "mime" @@ -1489,9 +1481,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "log", @@ -1501,9 +1493,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", @@ -1531,7 +1523,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases 0.1.1", "libc", @@ -1539,11 +1531,11 @@ dependencies = [ [[package]] name = "nix" -version = "0.31.2" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -1555,7 +1547,7 @@ version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "crossbeam-channel", "filetime", "fsevent-sys", @@ -1579,9 +1571,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-traits" @@ -1594,9 +1586,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -1606,7 +1598,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openheim" -version = "0.2.1" +version = "0.3.0" dependencies = [ "agent-client-protocol", "agent-client-protocol-tokio", @@ -1640,15 +1632,14 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.75" +version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "foreign-types", "libc", - "once_cell", "openssl-macros", "openssl-sys", ] @@ -1666,15 +1657,15 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.111" +version = "0.9.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" dependencies = [ "cc", "libc", @@ -1712,7 +1703,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link", ] @@ -1725,9 +1716,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pastey" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a797f0e07bdf071d15742978fc3128ec6c22891c31a3a931513263904c982a" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" [[package]] name = "percent-encoding" @@ -1737,18 +1728,18 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", @@ -1757,27 +1748,21 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -1797,11 +1782,21 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -1813,8 +1808,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e842efad9119158434d193c6682e2ebee4b44d6ad801d7b349623b3f57cdf55" dependencies = [ "futures", - "indexmap 2.13.0", - "nix 0.31.2", + "indexmap 2.14.0", + "nix 0.31.3", "tokio", "tracing", "windows", @@ -1822,9 +1817,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.43" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1835,6 +1830,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 = "radix_trie" version = "0.2.1" @@ -1847,9 +1848,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", "rand_core", @@ -1880,7 +1881,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cassowary", "compact_str", "crossterm", @@ -1901,16 +1902,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "redox_syscall" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" -dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", ] [[package]] @@ -1946,9 +1938,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -1957,9 +1949,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -2003,9 +1995,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", @@ -2051,9 +2043,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12ca9067b5ebfbd5b3fcdc4acfceb81aa7d5ab2a879dff7cb75d22434276aad" +checksum = "0810a9f717d9828f475fe1f629f4c305c8464b7f496c3a854b58d29e65f4058e" dependencies = [ "async-trait", "base64", @@ -2063,7 +2055,7 @@ dependencies = [ "pastey", "pin-project-lite", "process-wrap", - "reqwest 0.13.3", + "reqwest 0.13.4", "rmcp-macros", "schemars 1.2.1", "serde", @@ -2078,11 +2070,11 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7caa6743cc0888e433105fe1bc551a7f607940b126a37bc97b478e86064627eb" +checksum = "6aefac48c364756e97f04c0401ba3231e8607882c7c1d92da0437dc16307904d" dependencies = [ - "darling 0.23.0", + "darling", "proc-macro2", "quote", "serde_json", @@ -2110,7 +2102,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2119,22 +2111,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "once_cell", "rustls-pki-types", @@ -2145,18 +2137,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.2" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -2175,7 +2167,7 @@ version = "14.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "clipboard-win", "fd-lock", @@ -2193,9 +2185,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -2208,9 +2200,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -2261,12 +2253,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.11.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.10.0", - "core-foundation", + "bitflags 2.11.1", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -2274,9 +2266,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -2284,9 +2276,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -2331,9 +2323,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -2376,15 +2368,16 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.19.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" dependencies = [ "base64", + "bs58", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.13.0", + "indexmap 2.14.0", "schemars 0.9.0", "schemars 1.2.1", "serde_core", @@ -2395,11 +2388,11 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.19.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" dependencies = [ - "darling 0.23.0", + "darling", "proc-macro2", "quote", "syn", @@ -2433,9 +2426,9 @@ checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook" @@ -2454,7 +2447,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", - "mio 1.1.1", + "mio 1.2.1", "signal-hook", ] @@ -2470,9 +2463,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -2482,12 +2475,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2572,9 +2565,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2603,12 +2596,12 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.10.0", - "core-foundation", + "bitflags 2.11.1", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -2624,14 +2617,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -2697,23 +2690,38 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" -version = "1.49.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", - "mio 1.1.1", + "mio 1.2.1", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -2724,9 +2732,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -2817,7 +2825,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "serde", "serde_spanned", "toml_datetime", @@ -2849,20 +2857,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -2923,9 +2931,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -2963,21 +2971,21 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-truncate" @@ -3040,11 +3048,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.20.0" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -3095,18 +3103,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.120" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -3117,9 +3134,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.70" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ "js-sys", "wasm-bindgen", @@ -3127,9 +3144,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.120" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3137,9 +3154,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.120" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -3150,13 +3167,35 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.120" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.5.0" @@ -3170,11 +3209,23 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + [[package]] name = "web-sys" -version = "0.3.97" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -3350,15 +3401,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -3392,30 +3434,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows-threading" version = "0.2.1" @@ -3437,12 +3462,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -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" @@ -3455,12 +3474,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -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" @@ -3473,24 +3486,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -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" @@ -3503,12 +3504,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -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" @@ -3521,12 +3516,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -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" @@ -3539,12 +3528,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -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" @@ -3558,37 +3541,119 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" +name = "winnow" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] [[package]] -name = "winnow" -version = "0.7.14" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "memchr", + "wit-bindgen-rust-macro", ] [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.14.0", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -3597,9 +3662,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -3609,18 +3674,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.33" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.33" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", @@ -3629,18 +3694,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -3656,9 +3721,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -3667,9 +3732,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -3678,9 +3743,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", @@ -3689,6 +3754,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.14" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 32bbbd0..548a543 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openheim" -version = "0.2.1" +version = "0.3.0" edition = "2024" description = "A fast, multi-provider LLM agent runtime written in Rust" license = "MIT" diff --git a/README.md b/README.md index 47565db..b19fcd1 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,11 @@ Openheim is built in Rust from the ground up: - **Multi-provider** — OpenAI, Anthropic Claude, Google Gemini, and any OpenAI-compatible endpoint (Ollama, vLLM, LM Studio, etc.) - **Tool execution** — built-in shell, file read, and file write tools. Trait-based, so you can add your own. +- **Agent sandboxing** — configurable work-directory boundary restricts file access to a directory tree. Shell execution can be disabled entirely via `allow_shell = false` in config or `.allow_shell(false)` in the builder. - **MCP (Model Context Protocol)** — connect external MCP servers (stdio or Streamable HTTP) and their tools are automatically exposed to the LLM as `{server_name}__{tool_name}`. - **Conversation memory** — conversations (including full tool call history) persist to disk and resume across sessions -- **Skills** — drop a markdown file into `~/.openheim/skills/` and it's prepended to the system prompt. ACP clients can also pass skills per-session via `_meta`. +- **System identity** — edit `~/.openheim/system.md` to define how the agent presents itself. Required when preparing a session (created by `openheim init`). +- **Skills** — drop a markdown file into `~/.openheim/skills/` and it's injected into the system prompt. Set `default_skills` in config to auto-load skills every session; pass `--skills` for per-session additions. ACP clients can also pass skills per-session via `_meta`. - **ACP transport** — implements the [Agent Client Protocol](https://github.com/block/agent-client-protocol) over stdio (for editor integrations) and WebSocket (for remote clients), with real-time streaming of message chunks and tool calls - **Unified WebSocket** — single multiplexed `WS /ws` connection carries both ACP agent traffic (sessions, streaming, tool calls) and filesystem operations (file CRUD, live watching) via channel envelopes - **Retry with backoff** — transient failures (429s, 5xx, network errors) are retried automatically with exponential backoff @@ -79,11 +81,12 @@ cargo build --release ### Configure ```bash -# Generate the default config -cargo run -- init +# Generate the default config and system.md +openheim init -# Edit it +# Edit them vim ~/.openheim/config.toml +vim ~/.openheim/system.md ``` Example config: @@ -92,6 +95,15 @@ Example config: default_provider = "anthropic" max_iterations = 10 +# Skills loaded in every new session automatically (no --skills flag needed) +# default_skills = ["rules"] + +# Restrict the agent to a specific directory tree (defaults to invocation directory) +# work_dir = "/home/user/projects/myproject" + +# Set to false to remove the shell tool from the LLM's tool list entirely +# allow_shell = true + [providers.anthropic] api_base = "https://api.anthropic.com/v1" default_model = "claude-sonnet-4-6" @@ -129,26 +141,26 @@ models = ["llama3", "mistral", "codellama"] ```bash # Interactive REPL (default — no subcommand) -cargo run +openheim # Load skills in the REPL -cargo run -- --skills rust,debugging +openheim --skills rust,debugging # Single headless prompt, streams to stdout -cargo run -- run "List the files in the current directory" +openheim run "List the files in the current directory" # Single headless prompt with a model override -cargo run -- run "Hello" --model gpt-4o +openheim run "Hello" --model gpt-4o # ACP stdio agent (for Zed, Claude Code, and other ACP clients) -cargo run -- acp +openheim acp # ACP-over-WebSocket server -cargo run -- serve -cargo run -- serve --host 0.0.0.0 --port 1217 +openheim serve +openheim serve --host 0.0.0.0 --port 1217 # Initialize config -cargo run -- init +openheim init ``` --- @@ -170,15 +182,46 @@ Conversations are saved to `~/.openheim/history/` as JSON after every run. --- -## Skills +## Agent identity and skills + +### `~/.openheim/system.md` + +This file defines the agent's base identity. It is loaded when preparing each session (via `prepare()` / session setup) and is required — run `openheim init` to create it, then edit it freely. -Skills are markdown files in `~/.openheim/skills/`. When loaded, their content is injected into the system prompt before the conversation starts. +```markdown +You are a senior software engineer who writes clean, idiomatic code. +You prefer simple solutions and ask clarifying questions before making large changes. +``` + +### Skills -Use them to give the agent a persona, a set of coding standards, domain knowledge, or anything you'd otherwise paste into the system prompt every time. +Skills are markdown files in `~/.openheim/skills/`. They are injected into the system prompt after the identity block. ```bash -# Run the REPL with specific skills loaded -cargo run -- --skills rust,debugging +# Run with specific skills for this session +openheim --skills rust,debugging + +# Always load certain skills (set in config.toml) +# default_skills = ["rules", "concise"] +``` + +The system message the LLM receives is assembled in this order: + +``` +You are a general purpose multiprovider LLM agent. + +--- + +The user has given you the following identity: + + + +--- + +These are the skills you have mastered: + +### rust + ``` ACP clients (Zed, Claude Code, etc.) can pass skills per-session by including a `skills` array in the `_meta` field of the `NewSession` request — no flag needed on the server side. @@ -187,7 +230,7 @@ ACP clients (Zed, Claude Code, etc.) can pass skills per-session by including a ## Server mode -Start with `cargo run -- serve` (defaults to `0.0.0.0:1217`). +Start with `openheim serve` (defaults to `0.0.0.0:1217`). The server speaks the [Agent Client Protocol](https://github.com/block/agent-client-protocol) over WebSocket and exposes a multiplexed WS endpoint plus REST API routes: @@ -281,10 +324,12 @@ src/ retry.rs Automatic retry with exponential backoff tools/ Tool trait, registry, and built-in tools execute_command.rs / read_file.rs / write_file.rs + sandbox.rs Work-directory path validation + sandboxed_executor.rs Per-session executor wrapper enforcing work_dir and allow_shell mcp/ MCP (Model Context Protocol) client integration client.rs MCP server connection (stdio + Streamable HTTP) tool_handler.rs Adapts MCP tools to the ToolHandler trait - rag/ Conversation history, prompt builder, and skills manager + rag/ Conversation history, prompt builder, skills manager, and system identity acp/ ACP agent core — session state and protocol handling transport/ stdio.rs ACP-over-stdio transport (for editor integrations) @@ -298,7 +343,7 @@ src/ ## Development ```bash -RUST_LOG=debug cargo run -- run "test" +RUST_LOG=debug openheim run "test" cargo test cargo fmt --check cargo clippy diff --git a/docs/architecture.md b/docs/architecture.md index 2c64dfc..d4ace71 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -44,10 +44,11 @@ src/ ├── error.rs Unified Error / Result types │ ├── rag/ Retrieval-Augmented Generation utilities -│ ├── mod.rs RagContext — history + skills bundled together +│ ├── mod.rs RagContext — history + skills + system identity │ ├── history.rs HistoryManager — conversation persistence │ ├── skills.rs SkillsManager — Markdown skill files -│ └── prompt.rs PromptBuilder — injects skills as a system message +│ ├── system.rs SystemLoader — reads ~/.openheim/system.md +│ └── prompt.rs PromptBuilder — assembles structured system message │ ├── tools/ Tool abstraction and built-in implementations │ ├── mod.rs ToolHandler / ToolExecutor traits, SystemToolExecutor @@ -108,8 +109,10 @@ User / Client │ rag::RagContext::prepare │ │ │ │ 1. Load conversation from disk (history) │ -│ 2. Load skill files by name │ -│ 3. Build PromptBuilder (system message) │ +│ 2. Load ~/.openheim/system.md (identity) │ +│ 3. Merge default_skills + session skills │ +│ 4. Load skill files by name │ +│ 5. Build PromptBuilder (system message) │ │ │ │ Returns: (Conversation, PromptBuilder) │ └────────────────────┬────────────────────────┘ @@ -160,7 +163,8 @@ All persistence lives under `~/.openheim/` by default. ``` ~/.openheim/ -├── config.toml Agent configuration (providers, MCP servers, …) +├── config.toml Agent configuration (providers, MCP servers, default_skills, …) +├── system.md Agent identity — loaded on every session (required) ├── history/ │ ├── {uuid}.json One file per conversation │ └── … @@ -169,7 +173,7 @@ All persistence lives under `~/.openheim/` by default. └── … ``` -`HistoryManager` reads and writes conversation JSON files. `SkillsManager` reads `.md` files from the skills directory. Both paths are configurable at construction time, which is how the test suite uses temporary directories. +`SystemLoader` reads `system.md` on every `prepare()` call — missing file is a hard error (run `openheim init` to create it). `HistoryManager` reads and writes conversation JSON files. `SkillsManager` reads `.md` files from the skills directory. Both history and skills paths are configurable at construction time, which is how the test suite uses temporary directories. --- diff --git a/docs/configuration.md b/docs/configuration.md index fa89423..c1215f6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -3,7 +3,7 @@ Openheim loads its configuration from `~/.openheim/config.toml`. Generate a default file with: ```bash -cargo run -- init +openheim init ``` --- @@ -14,13 +14,31 @@ cargo run -- init |---|---|---|---| | `default_provider` | string | — | Provider to use when no `--model` override is given (must match a key under `[providers]`) | | `max_iterations` | integer | `10` | Maximum number of agent loop iterations per prompt before stopping | +| `default_skills` | string[] | `[]` | Skills loaded automatically in every new session. Merged with per-session `--skills`; defaults appear first, duplicates removed. | | `theme_color` | string | `"white"` | TUI accent color. Valid values: `white`, `gray`, `blue`, `cyan`, `magenta`, `green`, `yellow`, `red`, `pink`. Can also be changed at runtime with `:theme` | +| `work_dir` | path | cwd at invocation | Root directory the agent is allowed to read and write. The agent cannot access files outside this tree. When unset, defaults to the directory from which openheim was invoked. | +| `allow_shell` | boolean | `true` | Whether to expose the `execute_command` shell tool to the LLM. Set to `false` to remove the tool entirely — the LLM will not see it in its tool list. | ```toml default_provider = "anthropic" max_iterations = 20 + +# Always load these skills without passing --skills each time +default_skills = ["rules", "concise"] + +# Restrict the agent to a specific directory tree +work_dir = "/home/user/projects/myproject" + +# Disable shell command execution +allow_shell = false ``` +### Security notes + +**`work_dir`** is enforced at the application layer for `read_file` and `write_file`. Symlinks are followed and canonicalized so they cannot be used to escape the boundary. Shell commands (`execute_command`) are launched with `work_dir` as their working directory so relative paths resolve correctly, but absolute paths inside a shell command are not blocked — OS-level sandboxing (chroot, containers) is required for full shell isolation. + +**`allow_shell`** removes `execute_command` from the tool list sent to the LLM. When `false`, the LLM never sees the tool and cannot request it. + --- ## `[providers.]` @@ -111,6 +129,12 @@ The `name` key is sanitized when building tool names: hyphens and spaces become default_provider = "anthropic" max_iterations = 15 +# Restrict the agent to this directory tree +work_dir = "/home/user/projects/myproject" + +# Disable shell command access +allow_shell = false + [providers.anthropic] api_base = "https://api.anthropic.com/v1" default_model = "claude-sonnet-4-6" diff --git a/docs/library.md b/docs/library.md index 4ae7975..dde7719 100644 --- a/docs/library.md +++ b/docs/library.md @@ -84,6 +84,26 @@ Default models when `.model()` is omitted: - `"gemini"` → `gemini-2.0-flash` - everything else → `gpt-4o` +### Security controls + +Two builder methods control the agent's access boundary. Both override the corresponding `config.toml` fields when set. + +```rust +let client = OpenheimClient::builder() + .provider("openai") + .api_key("sk-...") + // Restrict file access to this directory tree + .work_dir("/home/user/projects/myproject") + // Remove the execute_command tool from the LLM's tool list entirely + .allow_shell(false) + .build() + .await?; +``` + +**`.work_dir(path)`** — sets the root directory the agent may read and write. The agent cannot access files outside this tree. Relative paths in tool arguments are resolved against this directory. Defaults to the directory from which the process was invoked when not set in the builder or config file. + +**`.allow_shell(bool)`** — controls whether the `execute_command` tool is exposed to the LLM. When `false` the tool is removed from the tool list entirely; the LLM never sees it and cannot request it. Defaults to `true`. + ### With MCP servers MCP servers can be added in either mode. Their tools become available to the agent automatically as `{server_name}__{tool_name}`. diff --git a/docs/skills.md b/docs/skills.md index e9dd71b..72cffe3 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -1,10 +1,33 @@ -# Skills +# Skills & Agent Identity -Skills are Markdown files that inject system-level instructions into every LLM request in a session. They let you shape the agent's behaviour — tone, domain expertise, constraints, output format — without touching code or configuration. +This document covers two related concepts: + +- **System identity** (`~/.openheim/system.md`) — the agent's base character, loaded on every session. +- **Skills** (`~/.openheim/skills/*.md`) — domain-specific instructions injected on demand. + +--- + +## Agent Identity (`system.md`) + +`~/.openheim/system.md` defines who the agent is. It is loaded at the start of every session and is **required** — openheim will error on startup if the file is missing. + +Run `openheim init` to create it with a default starting point, then edit it to match how you want the agent to behave: + +```markdown +You are a senior software engineer specialising in Rust and distributed systems. +You write clean, idiomatic code and prefer simple solutions over clever ones. +Ask clarifying questions before making large changes. +``` + +The identity is placed at the top of the system message, before any skills. --- -## Where skills live +## Skills + +Skills are Markdown files that inject domain-specific instructions into the system prompt. They let you shape the agent's behaviour — tone, domain expertise, constraints, output format — without touching code or configuration. + +### Where skills live Skills are stored as `.md` files in `~/.openheim/skills/`: @@ -19,23 +42,32 @@ The filename (without extension) is the skill name. Names are case-sensitive and --- -## How they work +## How the system message is assembled -When a session starts with one or more skills, `SkillsManager` reads each file and passes the content to `PromptBuilder`. Before each LLM call, the builder prepends a single system message containing all active skills, joined by `---` separators: +When a session has an identity and skills, the LLM receives a single system message structured like this: ``` -[System message] -## Skill: rust +You are a general purpose multiprovider LLM agent. + +--- -You are an expert Rust programmer. Always prefer idiomatic Rust… +The user has given you the following identity: + + --- -## Skill: tdd +These are the skills you have mastered: + +### rust -Write tests first. Every function should have at least one test… + + +--- -[Conversation history follows] +### tdd + + ``` The system message is reconstructed on every LLM call in the session, so it is always present regardless of how long the conversation runs. @@ -72,21 +104,23 @@ This skill is about systems programming. The assistant knows Rust and C. ## Enabling skills -### Via CLI +### Default skills (loaded every session) -Pass `--skills` as a comma-separated list of skill names: +Set `default_skills` in `~/.openheim/config.toml` to load skills automatically in every new session — no `--skills` flag needed: -```bash -openheim --skills rust,tdd -openheim run --skills concise "Summarise the project structure" +```toml +default_skills = ["rules", "concise"] ``` -### Via configuration +Default skills are merged with any per-session skills. Duplicates are deduplicated; defaults always appear first. -Set skills globally in `~/.openheim/config.toml` so they apply to every session: +### Per-session via CLI -```toml -skills = ["rust", "concise"] +Pass `--skills` as a comma-separated list of skill names: + +```bash +openheim --skills rust,tdd +openheim run --skills concise "Summarise the project structure" ``` ### Via the Rust library @@ -108,9 +142,6 @@ Skills are persisted in the conversation metadata (`ConversationMeta.skills`), s ## Listing available skills ```bash -# CLI -openheim --skills "" # lists available skills on startup (TUI) - # API (REST when running openheim serve) curl http://localhost:1217/api/skills # → ["concise","rust","tdd"] @@ -125,7 +156,15 @@ let skills = client.rag().skills.list_skills()?; --- -## Example skills +## Example files + +### `~/.openheim/system.md` + +```markdown +You are a pragmatic software engineer who writes clean, well-tested code. +You prefer simple solutions and always consider the maintenance burden of your suggestions. +When you are uncertain, say so rather than guessing. +``` ### `~/.openheim/skills/concise.md` diff --git a/src/acp/mod.rs b/src/acp/mod.rs index 6a046fc..829d08d 100644 --- a/src/acp/mod.rs +++ b/src/acp/mod.rs @@ -31,7 +31,7 @@ use crate::{ error::{Error, Result}, llm::LlmClient, rag::RagContext, - tools::{SystemToolExecutor, ToolExecutor}, + tools::{SandboxedExecutor, SystemToolExecutor, ToolExecutor}, }; use session::SessionState; @@ -45,6 +45,10 @@ pub struct AgentState { pub app_config: AppConfig, pub rag: RagContext, pub mcp_statuses: Vec, + /// Resolved work directory used as the sandbox boundary for every session. + pub work_dir: PathBuf, + /// Whether shell command execution is enabled for the LLM. + pub allow_shell: bool, sessions: Sessions, } @@ -52,8 +56,18 @@ impl AgentState { pub async fn new(config: AgentConfig, app_config: AppConfig, rag: RagContext) -> Result { let http_client = build_http_client(config.timeout_secs)?; let llm = create_client(&config, &http_client); - let (sys_executor, mcp_statuses) = SystemToolExecutor::build(&app_config.mcp_servers).await; + let allow_shell = app_config.allow_shell; + let (sys_executor, mcp_statuses) = + SystemToolExecutor::build(&app_config.mcp_servers, allow_shell).await; let executor = Arc::new(sys_executor) as Arc; + let work_dir = match app_config.work_dir.clone() { + Some(wd) => wd, + None => std::env::current_dir().map_err(|e| { + crate::error::Error::Other(format!( + "failed to determine current directory for work_dir: {e}" + )) + })?, + }; Ok(Self { llm, executor, @@ -61,6 +75,8 @@ impl AgentState { app_config, rag, mcp_statuses, + work_dir, + allow_shell, sessions: Arc::new(RwLock::new(HashMap::new())), }) } @@ -162,9 +178,14 @@ impl AgentState { } else { self.llm.clone() }; + let sandboxed = Arc::new(SandboxedExecutor::new( + self.executor.clone(), + self.work_dir.clone(), + self.allow_shell, + )) as Arc; ( llm, - self.executor.clone(), + sandboxed, s.config.clone(), s.chat_id, s.skills.clone(), diff --git a/src/client.rs b/src/client.rs index cdb96ac..1bfc198 100644 --- a/src/client.rs +++ b/src/client.rs @@ -239,6 +239,9 @@ pub struct OpenheimBuilder { timeout_secs: Option, max_tokens: Option, mcp_servers: BTreeMap, + default_skills: Vec, + work_dir: Option, + allow_shell: Option, } impl OpenheimBuilder { @@ -297,6 +300,27 @@ impl OpenheimBuilder { self } + /// Skills loaded automatically in every new session. + pub fn default_skills(mut self, skills: Vec) -> Self { + self.default_skills = skills; + self + } + + /// Root directory the agent is allowed to read/write. + /// Overrides `work_dir` from the config file. When not set, defaults to the + /// directory from which the process was invoked. + pub fn work_dir(mut self, path: impl Into) -> Self { + self.work_dir = Some(path.into()); + self + } + + /// Whether to expose the `execute_command` shell tool to the LLM. + /// Overrides `allow_shell` from the config file. Defaults to `false`. + pub fn allow_shell(mut self, allow: bool) -> Self { + self.allow_shell = Some(allow); + self + } + /// Build the client, connecting to MCP servers and initialising the agent state. pub async fn build(self) -> Result { let (agent_config, mut app_config) = if self.provider.is_some() @@ -312,6 +336,7 @@ impl OpenheimBuilder { self.max_iterations, self.timeout_secs, self.max_tokens, + self.default_skills.clone(), ) } else { let app_config = match self.config_path { @@ -336,12 +361,40 @@ impl OpenheimBuilder { app_config.mcp_servers.insert(name, cfg); } - let rag = RagContext::new()?; + // Apply builder default_skills for the file-based path (programmatic path sets them directly) + if !self.default_skills.is_empty() { + app_config.default_skills = self.default_skills; + } + + if let Some(wd) = self.work_dir { + let abs = if wd.is_absolute() { + wd.clone() + } else { + std::env::current_dir() + .map_err(|e| { + crate::error::Error::Other(format!("cannot resolve relative work_dir: {e}")) + })? + .join(&wd) + }; + let canonical = abs.canonicalize().map_err(|e| { + crate::error::Error::Other(format!( + "work_dir '{}' is inaccessible: {e}", + wd.display() + )) + })?; + app_config.work_dir = Some(canonical); + } + if let Some(shell) = self.allow_shell { + app_config.allow_shell = shell; + } + + let rag = RagContext::new(app_config.default_skills.clone())?; let state = Arc::new(AgentState::new(agent_config, app_config, rag).await?); Ok(OpenheimClient { state }) } } +#[allow(clippy::too_many_arguments)] fn build_programmatic( provider: Option, api_key: Option, @@ -350,6 +403,7 @@ fn build_programmatic( max_iterations: Option, timeout_secs: Option, max_tokens: Option, + default_skills: Vec, ) -> (AgentConfig, AppConfig) { let provider = provider.unwrap_or_else(|| "openai".to_string()); let api_base = api_base.unwrap_or_else(|| default_api_base(&provider)); @@ -378,6 +432,9 @@ fn build_programmatic( theme_color: None, providers, mcp_servers: BTreeMap::new(), + default_skills, + work_dir: None, + allow_shell: false, }; let agent_config = AgentConfig { diff --git a/src/config/client.rs b/src/config/client.rs index dab187a..2596d90 100644 --- a/src/config/client.rs +++ b/src/config/client.rs @@ -121,6 +121,9 @@ mod tests { theme_color: None, providers, mcp_servers: BTreeMap::new(), + default_skills: vec![], + work_dir: None, + allow_shell: true, } } diff --git a/src/config/config.toml.default b/src/config/config.toml.default index ab77dd0..005337f 100644 --- a/src/config/config.toml.default +++ b/src/config/config.toml.default @@ -7,6 +7,18 @@ default_provider = "openai" # Maximum number of agent iterations (can be overridden with --max-iterations) max_iterations = 10 +# Skills loaded automatically in every new session (no --skills flag needed). +# Add skill names matching files in ~/.openheim/skills/{name}.md +# default_skills = ["rules"] + +# Root directory the agent is allowed to read/write. +# When unset the directory from which openheim is invoked is used. +# work_dir = "/home/user/projects/myproject" + +# Whether to allow the LLM to execute shell commands via the execute_command tool. +# Set to false to remove the tool from the LLM's view entirely. +# allow_shell = true + # --- Provider Configuration --- # # The provider name determines which API client is used: diff --git a/src/config/mod.rs b/src/config/mod.rs index c0f6646..ce9d4da 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -23,20 +23,43 @@ pub fn config_path() -> Result { Ok(config_dir()?.join("config.toml")) } +const DEFAULT_SYSTEM_MD: &str = "You are Openheim, a multipurpose, multiprovider LLM agent."; + /// Initialize the config file at ~/.openheim/config.toml with the default template. -/// Returns the path written to. +/// Also writes ~/.openheim/system.md if it does not already exist. +/// Returns the path of the config file written. +/// +/// Errors if `config.toml` already exists. `system.md` is written regardless — +/// so existing users who already have a config can still run `openheim init` to +/// get their `system.md` created. pub fn init_config() -> Result { let dir = config_dir()?; std::fs::create_dir_all(&dir)?; - let path = dir.join("config.toml"); - if path.exists() { + + // Always write system.md first so existing users who re-run `init` get it + // even though config.toml already exists and will cause an early return below. + let system_path = dir.join("system.md"); + let system_written = !system_path.exists(); + if system_written { + std::fs::write(&system_path, DEFAULT_SYSTEM_MD)?; + } + + let config_path = dir.join("config.toml"); + if config_path.exists() { + let system_note = if system_written { + format!("system.md has been created at {}.", system_path.display()) + } else { + format!("system.md is available at {}.", system_path.display()) + }; return Err(Error::config(format!( - "Config file already exists at {}", - path.display() + "Config file already exists at {}. {}", + config_path.display(), + system_note ))); } - std::fs::write(&path, DEFAULT_CONFIG)?; - Ok(path) + std::fs::write(&config_path, DEFAULT_CONFIG)?; + + Ok(config_path) } /// Load AppConfig from a specific path diff --git a/src/config/resolve.rs b/src/config/resolve.rs index 36243f0..6625c4f 100644 --- a/src/config/resolve.rs +++ b/src/config/resolve.rs @@ -149,6 +149,9 @@ mod tests { theme_color: None, providers, mcp_servers: BTreeMap::new(), + default_skills: vec![], + work_dir: None, + allow_shell: true, } } @@ -189,6 +192,9 @@ mod tests { theme_color: None, providers: BTreeMap::new(), mcp_servers: BTreeMap::new(), + default_skills: vec![], + work_dir: None, + allow_shell: true, }; let err = config.resolve(None).unwrap_err(); assert!(err.to_string().contains("nonexistent")); diff --git a/src/config/types.rs b/src/config/types.rs index e3cfaf3..831dbfb 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; +use std::path::PathBuf; use std::sync::Arc; /// Public model info for a single provider (no credentials). @@ -38,6 +39,21 @@ pub struct AppConfig { pub providers: BTreeMap, #[serde(default)] pub mcp_servers: BTreeMap, + /// Skills loaded automatically in every new session (merged with --skills at runtime). + #[serde(default)] + pub default_skills: Vec, + /// Root directory the agent is allowed to read/write. Defaults to the + /// directory from which openheim was invoked when not set. + #[serde(default)] + pub work_dir: Option, + /// Whether to expose the `execute_command` shell tool to the LLM. + /// Defaults to `false`. Set to `true` to explicitly opt in to shell access. + #[serde(default = "default_allow_shell")] + pub allow_shell: bool, +} + +fn default_allow_shell() -> bool { + false } /// Configuration for a single MCP server connection. diff --git a/src/mcp/client.rs b/src/mcp/client.rs index ec39d61..9e17448 100644 --- a/src/mcp/client.rs +++ b/src/mcp/client.rs @@ -42,7 +42,9 @@ impl McpClient { for (k, v) in &config.env { cmd.env(k, v); } - let transport = TokioChildProcess::new(cmd) + let (transport, _) = TokioChildProcess::builder(cmd) + .stderr(std::process::Stdio::null()) + .spawn() .map_err(|e| Error::Other(format!("MCP spawn '{}' failed: {}", name, e)))?; let service = ().serve(transport).await.map_err(|e| { Error::Other(format!("MCP stdio connect to '{}' failed: {}", name, e)) diff --git a/src/rag/mod.rs b/src/rag/mod.rs index 284d608..ad85322 100644 --- a/src/rag/mod.rs +++ b/src/rag/mod.rs @@ -3,10 +3,12 @@ pub mod history; pub mod prompt; pub mod skills; +pub mod system; pub use history::{Conversation, ConversationMeta, HistoryManager}; pub use prompt::PromptBuilder; pub use skills::SkillsManager; +pub use system::SystemLoader; use crate::error::Result; use uuid::Uuid; @@ -18,22 +20,33 @@ pub struct RagContext { pub history: HistoryManager, /// Named skill files loaded from `~/.openheim/skills/`. pub skills: SkillsManager, + /// System identity loaded from `~/.openheim/system.md`. + pub system: SystemLoader, + /// Skills included in every new session (from `default_skills` in config). + default_skills: Vec, } impl RagContext { - /// Initialise history and skills from the default openheim data directory. - pub fn new() -> Result { + /// Initialise history, skills, and system identity from the default openheim data directory. + /// + /// `default_skills` are merged with any per-session skills on each new conversation. + pub fn new(default_skills: Vec) -> Result { Ok(Self { history: HistoryManager::new()?, skills: SkillsManager::new()?, + system: SystemLoader::new()?, + default_skills, }) } /// Load or create a conversation and build the prompt context for an agent turn. /// /// Returns the resolved [`Conversation`] and a [`PromptBuilder`] already populated - /// with any requested skills. When `chat_id` refers to an existing conversation the - /// skills stored on that conversation take precedence over `skill_names`. + /// with the system identity and any requested skills. + /// + /// For **new** conversations, `default_skills` are merged with `skill_names` (defaults + /// first, deduplicated) and persisted on the conversation. For **existing** conversations + /// the stored skill list is used as-is, preserving the state from when the session began. pub fn prepare( &self, chat_id: Option, @@ -41,22 +54,27 @@ impl RagContext { model: Option, provider: Option, ) -> Result<(Conversation, PromptBuilder)> { + // Always pass merged skills to resolve_conversation. For existing conversations + // the parameter is ignored (stored list wins); for new ones it gets persisted. + let merged_skills = merge_skills(&self.default_skills, skill_names); + let conversation = self.history - .resolve_conversation(chat_id, model, provider, skill_names.to_vec())?; + .resolve_conversation(chat_id, model, provider, merged_skills)?; let mut builder = PromptBuilder::new(); - // Load skills: use conversation's stored skills if continuing, otherwise use provided ones - let skills_to_load = if chat_id.is_some() && !conversation.meta.skills.is_empty() { - &conversation.meta.skills - } else { - skill_names - }; + // Always inject the system identity as the first layer. + let system_content = self.system.load()?; + tracing::debug!(chars = system_content.len(), "prepare: loaded system.md"); + builder.set_system(system_content); - if !skills_to_load.is_empty() { - let loaded = self.skills.load_skills(skills_to_load)?; + // Load skills from the conversation's stored list (already contains merged + // defaults for new conversations, or the original set for existing ones). + if !conversation.meta.skills.is_empty() { + let loaded = self.skills.load_skills(&conversation.meta.skills)?; for (name, content) in &loaded { + tracing::debug!(skill = %name, "prepare: loaded skill"); builder.add_skill(name, content); } } @@ -64,3 +82,47 @@ impl RagContext { Ok((conversation, builder)) } } + +/// Merge `defaults` with `session` skills, preserving order and deduplicating. +/// Defaults come first; session skills are appended only if not already present. +fn merge_skills(defaults: &[String], session: &[String]) -> Vec { + let mut merged = Vec::new(); + for s in defaults.iter().chain(session.iter()) { + if !merged.contains(s) { + merged.push(s.clone()); + } + } + merged +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn merge_skills_defaults_first_no_duplicates() { + let defaults = vec!["rules".to_string(), "coding".to_string()]; + let session = vec!["coding".to_string(), "rust".to_string()]; + let merged = merge_skills(&defaults, &session); + assert_eq!(merged, vec!["rules", "coding", "rust"]); + } + + #[test] + fn merge_skills_empty_defaults() { + let merged = merge_skills(&[], &["rust".to_string()]); + assert_eq!(merged, vec!["rust"]); + } + + #[test] + fn merge_skills_empty_session() { + let merged = merge_skills(&["rules".to_string()], &[]); + assert_eq!(merged, vec!["rules"]); + } + + #[test] + fn merge_skills_deduplicates_within_defaults() { + let defaults = vec!["rules".to_string(), "rules".to_string()]; + let merged = merge_skills(&defaults, &[]); + assert_eq!(merged, vec!["rules"]); + } +} diff --git a/src/rag/prompt.rs b/src/rag/prompt.rs index 072ea4e..928ea26 100644 --- a/src/rag/prompt.rs +++ b/src/rag/prompt.rs @@ -1,16 +1,18 @@ use crate::core::models::{Message, Role}; -/// Builds an LLM message sequence by prepending skill content as a system message. +/// Builds an LLM message sequence by prepending a structured system message. /// -/// Skills are accumulated with [`add_skill`] and combined into a single system -/// message joined by `---` separators. That message is inserted at position 0, -/// followed by the existing conversation history. +/// The system message is assembled from an optional identity (from `system.md`) +/// and any number of named skills, laid out in a consistent template so the +/// model always understands what it is, what identity it has been given, and +/// what skills it has been asked to master. /// -/// If no skills have been added, [`build`] returns the history unchanged with no -/// system message prepended. +/// If neither identity nor skills have been registered, no system message is +/// prepended and `history` is returned unchanged. #[derive(Default)] pub struct PromptBuilder { - system_parts: Vec, + system_identity: Option, + skills: Vec<(String, String)>, } impl PromptBuilder { @@ -18,35 +20,83 @@ impl PromptBuilder { Self::default() } - /// Registers a skill to be included in the system message. - /// - /// Each call appends a `## Skill: {name}` section. Multiple skills are joined - /// with `\n\n---\n\n` when [`build`] is called. + /// Sets the system identity loaded from `system.md`. + pub fn set_system(&mut self, content: String) { + self.system_identity = Some(content); + } + + /// Registers a named skill to include in the system message. pub fn add_skill(&mut self, name: &str, content: &str) { - self.system_parts - .push(format!("## Skill: {}\n\n{}", name, content)); + self.skills.push((name.to_string(), content.to_string())); } /// Constructs the full message list for an LLM request. /// - /// Prepends a system message containing all registered skill sections to - /// `history`. If no skills have been registered, `history` is returned as-is - /// with no system message added. + /// When there is substantive content (non-blank identity or at least one + /// skill), a `Role::System` message is inserted at position 0 using the + /// template below. Otherwise `history` is returned unchanged. + /// + /// ```text + /// You are a general purpose multiprovider LLM agent. + /// + /// The user has given you the following identity: + /// + /// + /// + /// --- + /// + /// These are the skills you have mastered: + /// + /// ### + /// + /// + /// ``` pub fn build(&self, history: &[Message]) -> Vec { - let mut messages = Vec::new(); - - if !self.system_parts.is_empty() { - let system_content = self.system_parts.join("\n\n---\n\n"); - messages.push(Message { - role: Role::System, - content: Some(system_content), - tool_calls: None, - tool_call_id: None, - tool_name: None, - is_error: false, - }); + let orig = self.system_identity.as_deref(); + let identity = orig.filter(|s| !s.trim().is_empty()); + let has_content = identity.is_some() || !self.skills.is_empty(); + + if !has_content { + return history.to_vec(); + } + + let mut sections: Vec = Vec::new(); + + sections.push("You are a general purpose multiprovider LLM agent.".to_string()); + + if let Some(id) = identity { + sections.push(format!( + "The user has given you the following identity:\n\n{id}" + )); } + if !self.skills.is_empty() { + let skill_blocks: Vec = self + .skills + .iter() + .map(|(name, content)| format!("### {name}\n\n{content}")) + .collect(); + sections.push(format!( + "These are the skills you have mastered:\n\n{}", + skill_blocks.join("\n\n---\n\n") + )); + } + + let system_content = sections.join("\n\n---\n\n"); + tracing::debug!( + len = system_content.len(), + "build: system message assembled" + ); + + let mut messages = vec![Message { + role: Role::System, + content: Some(system_content), + tool_calls: None, + tool_call_id: None, + tool_name: None, + is_error: false, + }]; + messages.extend_from_slice(history); messages } @@ -57,42 +107,84 @@ mod tests { use super::*; #[test] - fn build_with_no_skills_returns_history_unchanged() { + fn build_with_nothing_returns_history_unchanged() { let builder = PromptBuilder::new(); let history = vec![Message::user("hello".into())]; let result = builder.build(&history); assert_eq!(result.len(), 1); assert_eq!(result[0].role, Role::User); - assert_eq!(result[0].content.as_deref(), Some("hello")); } #[test] - fn build_with_one_skill_prepends_system_message() { + fn empty_system_identity_is_ignored() { let mut builder = PromptBuilder::new(); - builder.add_skill("coding", "You are a coding assistant."); - let history = vec![Message::user("help".into())]; + builder.set_system(" ".into()); + let history = vec![Message::user("hi".into())]; let result = builder.build(&history); + assert_eq!(result.len(), 1); + assert_eq!(result[0].role, Role::User); + } + + #[test] + fn set_system_alone_produces_structured_message() { + let mut builder = PromptBuilder::new(); + builder.set_system("I am a helpful agent.".into()); + let result = builder.build(&[Message::user("hi".into())]); assert_eq!(result.len(), 2); - assert_eq!(result[0].role, Role::System); let content = result[0].content.as_deref().unwrap(); - assert!(content.contains("## Skill: coding")); - assert!(content.contains("You are a coding assistant.")); - assert_eq!(result[1].role, Role::User); + assert!(content.contains("You are a general purpose multiprovider LLM agent.")); + assert!(content.contains("The user has given you the following identity:")); + assert!(content.contains("I am a helpful agent.")); + assert!(!content.contains("skills")); } #[test] - fn build_with_multiple_skills_joins_with_separator() { + fn skill_alone_produces_structured_message() { let mut builder = PromptBuilder::new(); - builder.add_skill("skill_a", "Content A"); - builder.add_skill("skill_b", "Content B"); - let history = vec![Message::user("test".into())]; - let result = builder.build(&history); + builder.add_skill("rust", "Write idiomatic Rust."); + let result = builder.build(&[Message::user("hi".into())]); + + assert_eq!(result.len(), 2); + let content = result[0].content.as_deref().unwrap(); + assert!(content.contains("You are a general purpose multiprovider LLM agent.")); + assert!(content.contains("These are the skills you have mastered:")); + assert!(content.contains("### rust")); + assert!(content.contains("Write idiomatic Rust.")); + assert!(!content.contains("identity")); + } + + #[test] + fn identity_and_skills_are_both_present_in_order() { + let mut builder = PromptBuilder::new(); + builder.set_system("Custom identity.".into()); + builder.add_skill("rust", "Be idiomatic."); + builder.add_skill("testing", "Write tests."); + let result = builder.build(&[Message::user("go".into())]); assert_eq!(result.len(), 2); let content = result[0].content.as_deref().unwrap(); - assert!(content.contains("## Skill: skill_a")); - assert!(content.contains("## Skill: skill_b")); + + let base_pos = content.find("general purpose").unwrap(); + let identity_pos = content.find("Custom identity.").unwrap(); + let skills_pos = content.find("skills you have mastered").unwrap(); + let rust_pos = content.find("### rust").unwrap(); + let testing_pos = content.find("### testing").unwrap(); + + assert!(base_pos < identity_pos); + assert!(identity_pos < skills_pos); + assert!(skills_pos < rust_pos); + assert!(rust_pos < testing_pos); + } + + #[test] + fn multiple_skills_are_separated() { + let mut builder = PromptBuilder::new(); + builder.add_skill("a", "Content A"); + builder.add_skill("b", "Content B"); + let content = builder.build(&[]).remove(0).content.unwrap(); + assert!(content.contains("### a")); + assert!(content.contains("### b")); assert!(content.contains("---")); } @@ -107,7 +199,7 @@ mod tests { ]; let result = builder.build(&history); - assert_eq!(result.len(), 4); // system + 3 history + assert_eq!(result.len(), 4); assert_eq!(result[0].role, Role::System); assert_eq!(result[1].content.as_deref(), Some("first")); assert_eq!(result[2].content.as_deref(), Some("second")); diff --git a/src/rag/system.rs b/src/rag/system.rs new file mode 100644 index 0000000..10f1f76 --- /dev/null +++ b/src/rag/system.rs @@ -0,0 +1,34 @@ +use crate::config::config_dir; +use crate::error::{Error, Result}; +use std::path::PathBuf; + +/// Loads the system identity from `~/.openheim/system.md`. +/// +/// The file must exist — run `openheim init` to create it. Returns an error +/// if the file is absent, mirroring the behaviour of a missing `config.toml`. +#[derive(Clone)] +pub struct SystemLoader { + path: PathBuf, +} + +impl SystemLoader { + /// Creates a `SystemLoader` pointed at `~/.openheim/system.md`. + pub fn new() -> Result { + Ok(Self { + path: config_dir()?.join("system.md"), + }) + } + + /// Returns the contents of `system.md`. + /// + /// Returns an error if the file does not exist. + pub fn load(&self) -> Result { + if !self.path.exists() { + return Err(Error::config(format!( + "system.md not found at {}. Run `openheim init` to create one.", + self.path.display() + ))); + } + Ok(std::fs::read_to_string(&self.path)?) + } +} diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 5b43ec1..de649c4 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -52,6 +52,8 @@ mod execute_command; mod read_file; +pub mod sandbox; +mod sandboxed_executor; mod write_file; use std::collections::{BTreeMap, HashMap}; @@ -62,6 +64,8 @@ use crate::config::McpServerConfig; use crate::core::models::Tool; use crate::error::{Error, Result}; +pub use sandboxed_executor::SandboxedExecutor; + #[async_trait] pub trait ToolHandler: Send + Sync { /// Returns the tool definition (name, description, JSON-schema parameters). @@ -107,13 +111,20 @@ impl SystemToolExecutor { /// Builds a fully-configured executor: registers built-in tools then connects /// to all configured MCP servers and registers their tools. /// + /// When `allow_shell` is `false` the `execute_command` tool is omitted so + /// the LLM never sees it in its tool list. + /// /// Returns the executor alongside [`McpServerStatus`] entries for each server /// so callers can inspect which connections succeeded. pub async fn build( mcp_configs: &BTreeMap, + allow_shell: bool, ) -> (Self, Vec) { let mut executor = Self::new(); executor.register_builtins(); + if !allow_shell { + executor.handlers.remove("execute_command"); + } let (handlers, statuses) = crate::mcp::load_mcp_tools(mcp_configs).await; for handler in handlers { executor.register(handler); @@ -164,6 +175,8 @@ impl ToolExecutor for SystemToolExecutor { #[cfg(test)] mod tests { + use std::collections::BTreeMap; + use super::*; #[test] @@ -189,4 +202,12 @@ mod tests { assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("Unknown tool")); } + + #[tokio::test] + async fn build_without_shell_omits_execute_command() { + let (executor, _) = SystemToolExecutor::build(&BTreeMap::new(), false).await; + assert!(!executor.handlers.contains_key("execute_command")); + assert!(executor.handlers.contains_key("read_file")); + assert!(executor.handlers.contains_key("write_file")); + } } diff --git a/src/tools/sandbox.rs b/src/tools/sandbox.rs new file mode 100644 index 0000000..71d7439 --- /dev/null +++ b/src/tools/sandbox.rs @@ -0,0 +1,139 @@ +//! Work-directory path validation for the agent sandbox. + +use std::path::{Path, PathBuf}; + +use crate::error::{Error, Result}; + +/// Validates that `requested` resolves to a path within `work_dir`. +/// +/// Relative paths are resolved against `work_dir`. For paths that already +/// exist symlinks are followed and the canonicalized result is checked. +/// For paths that do not yet exist (e.g. a file about to be written) the +/// nearest existing ancestor is canonicalized and checked instead. +/// +/// Returns the resolved absolute path on success, or an error describing +/// why the path is rejected. +pub fn validate_path(requested: &str, work_dir: &Path) -> Result { + let work_dir_canonical = work_dir.canonicalize().map_err(|_| { + Error::ToolExecutionError(format!( + "work directory '{}' is inaccessible", + work_dir.display() + )) + })?; + + let requested_path = Path::new(requested); + let resolved = if requested_path.is_absolute() { + requested_path.to_path_buf() + } else { + work_dir_canonical.join(requested_path) + }; + + let check = if resolved.exists() { + resolved.canonicalize().map_err(Error::IoError)? + } else { + // Dangling symlinks look non-existent to exists(); detect them explicitly + // so write_file cannot create the symlink target outside the sandbox. + if resolved + .symlink_metadata() + .ok() + .is_some_and(|m| m.file_type().is_symlink()) + { + return Err(Error::ToolExecutionError(format!( + "path '{}' is a dangling symlink (work directory: '{}')", + requested, + work_dir.display() + ))); + } + // Walk up the tree until we find an existing ancestor, canonicalize + // that, and verify it is within the work directory. + let mut ancestor: &Path = &resolved; + loop { + ancestor = ancestor.parent().ok_or_else(|| { + Error::ToolExecutionError(format!( + "path '{}' has no accessible ancestor within the filesystem", + requested + )) + })?; + if ancestor.exists() { + let canonical_ancestor = ancestor.canonicalize().map_err(Error::IoError)?; + if !canonical_ancestor.starts_with(&work_dir_canonical) { + return Err(Error::ToolExecutionError(format!( + "path '{}' is outside the work directory '{}'", + requested, + work_dir.display() + ))); + } + // The non-existing tail of the path is fine; return it as-is + // so the caller (write_file) can create it. + return Ok(resolved); + } + } + }; + + if check.starts_with(&work_dir_canonical) { + Ok(check) + } else { + Err(Error::ToolExecutionError(format!( + "path '{}' is outside the work directory '{}'", + requested, + work_dir.display() + ))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn allows_existing_file_inside_work_dir() { + let dir = tempfile::tempdir().unwrap(); + let file = dir.path().join("foo.txt"); + fs::write(&file, "x").unwrap(); + assert!(validate_path(file.to_str().unwrap(), dir.path()).is_ok()); + } + + #[test] + fn allows_relative_path_inside() { + let dir = tempfile::tempdir().unwrap(); + fs::create_dir(dir.path().join("sub")).unwrap(); + assert!(validate_path("sub", dir.path()).is_ok()); + } + + #[test] + fn allows_new_file_path_inside_work_dir() { + let dir = tempfile::tempdir().unwrap(); + assert!(validate_path("new_file.txt", dir.path()).is_ok()); + } + + #[test] + fn rejects_absolute_path_outside_work_dir() { + let dir = tempfile::tempdir().unwrap(); + let other = tempfile::tempdir().unwrap(); + let outside = other.path().join("secret.txt"); + fs::write(&outside, "x").unwrap(); + let err = validate_path(outside.to_str().unwrap(), dir.path()).unwrap_err(); + assert!(err.to_string().contains("outside the work directory")); + } + + #[test] + fn rejects_dotdot_traversal() { + let dir = tempfile::tempdir().unwrap(); + let err = validate_path("../../etc/passwd", dir.path()).unwrap_err(); + assert!(err.to_string().contains("outside the work directory")); + } + + #[test] + fn rejects_dangling_symlink_inside_work_dir() { + let dir = tempfile::tempdir().unwrap(); + let link = dir.path().join("dangling_link"); + // Point the symlink at a path that does not exist so it is dangling. + std::os::unix::fs::symlink("/nonexistent_target_path_12345", &link).unwrap(); + let err = validate_path(link.to_str().unwrap(), dir.path()).unwrap_err(); + assert!( + err.to_string().contains("dangling symlink"), + "unexpected error: {err}" + ); + } +} diff --git a/src/tools/sandboxed_executor.rs b/src/tools/sandboxed_executor.rs new file mode 100644 index 0000000..39dc4fd --- /dev/null +++ b/src/tools/sandboxed_executor.rs @@ -0,0 +1,141 @@ +//! Work-directory sandboxing wrapper around any [`ToolExecutor`]. + +use std::{path::PathBuf, sync::Arc}; + +use async_trait::async_trait; +use tokio::{fs, process::Command}; + +use crate::{ + core::models::Tool, + error::{Error, Result}, +}; + +use super::{ToolExecutor, sandbox::validate_path}; + +/// Wraps an inner [`ToolExecutor`] and enforces a work-directory boundary. +/// +/// The three built-in tools are intercepted: +/// - `read_file` / `write_file`: the requested path is validated to be within +/// `work_dir` (following symlinks for existing paths); access outside the +/// boundary is rejected with an error the LLM can read and react to. +/// - `execute_command`: when `allow_shell` is `false` the call is rejected +/// immediately. When `true` the command runs with its working directory set +/// to `work_dir` so relative paths behave correctly. Note that absolute +/// paths inside the shell command are not blocked at the application layer +/// — OS-level sandboxing is required for that. +/// +/// All other tools are forwarded to the inner executor unchanged. +pub struct SandboxedExecutor { + inner: Arc, + work_dir: Arc, + allow_shell: bool, +} + +impl SandboxedExecutor { + pub fn new(inner: Arc, work_dir: PathBuf, allow_shell: bool) -> Self { + Self { + inner, + work_dir: Arc::new(work_dir), + allow_shell, + } + } +} + +#[async_trait] +impl ToolExecutor for SandboxedExecutor { + fn list_tools(&self) -> Vec { + let tools = self.inner.list_tools(); + if self.allow_shell { + tools + } else { + tools + .into_iter() + .filter(|t| t.function.name != "execute_command") + .collect() + } + } + + async fn execute(&self, name: &str, args_json: &str) -> Result { + match name { + "read_file" => { + let args: serde_json::Value = serde_json::from_str(args_json) + .map_err(|e| Error::ParseError(format!("failed to parse arguments: {}", e)))?; + let path = args["path"] + .as_str() + .ok_or_else(|| Error::ParseError("missing 'path' argument".to_string()))?; + let validated = validate_path(path, &self.work_dir)?; + let content = fs::read_to_string(&validated) + .await + .map_err(Error::IoError)?; + Ok(content) + } + + "write_file" => { + let args: serde_json::Value = serde_json::from_str(args_json) + .map_err(|e| Error::ParseError(format!("failed to parse arguments: {}", e)))?; + let path = args["path"] + .as_str() + .ok_or_else(|| Error::ParseError("missing 'path' argument".to_string()))?; + let content = args["content"] + .as_str() + .ok_or_else(|| Error::ParseError("missing 'content' argument".to_string()))?; + let validated = validate_path(path, &self.work_dir)?; + if let Some(parent) = validated.parent() + && !parent.as_os_str().is_empty() + { + fs::create_dir_all(parent).await.map_err(Error::IoError)?; + } + fs::write(&validated, content) + .await + .map_err(Error::IoError)?; + Ok(format!("Successfully wrote to {}", validated.display())) + } + + "execute_command" => { + if !self.allow_shell { + return Err(Error::ToolExecutionError( + "shell command execution is disabled by configuration".to_string(), + )); + } + let args: serde_json::Value = serde_json::from_str(args_json) + .map_err(|e| Error::ParseError(format!("failed to parse arguments: {}", e)))?; + let command = args["command"] + .as_str() + .ok_or_else(|| Error::ParseError("missing 'command' argument".to_string()))?; + + #[cfg(target_family = "unix")] + let mut cmd = { + let mut c = Command::new("sh"); + c.arg("-c").arg(command); + c + }; + #[cfg(target_family = "windows")] + let mut cmd = { + let mut c = Command::new("cmd"); + c.arg("/C").arg(command); + c + }; + + cmd.current_dir(&*self.work_dir); + + let output = cmd.output().await.map_err(|e| { + Error::ToolExecutionError(format!("failed to execute command: {}", e)) + })?; + + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + + if output.status.success() { + Ok(stdout) + } else { + Err(Error::ToolExecutionError(format!( + "Command failed:\nStdout: {}\nStderr: {}", + stdout, stderr + ))) + } + } + + _ => self.inner.execute(name, args_json).await, + } + } +} diff --git a/src/transport/run.rs b/src/transport/run.rs index 5726620..db94a61 100644 --- a/src/transport/run.rs +++ b/src/transport/run.rs @@ -30,7 +30,7 @@ use crate::{ pub async fn run_headless(prompt: String, model: Option) -> crate::error::Result<()> { let app_config = load_config()?; let agent_config = app_config.resolve(model.as_deref())?; - let rag = RagContext::new()?; + let rag = RagContext::new(app_config.default_skills.clone())?; let state = Arc::new(AgentState::new(agent_config, app_config, rag).await?); let (server_half, client_half) = tokio::io::duplex(65536); @@ -82,8 +82,6 @@ pub async fn run_headless(prompt: String, model: Option) -> crate::error .await .map_err(|e| crate::error::Error::Other(e.to_string()))?; - server_handle - .await - .map_err(|e| crate::error::Error::Other(e.to_string())) - .and_then(|r| r.map_err(|e| crate::error::Error::Other(e.to_string()))) + server_handle.abort(); + Ok(()) } diff --git a/src/transport/stdio.rs b/src/transport/stdio.rs index c1ec0fd..ff738a6 100644 --- a/src/transport/stdio.rs +++ b/src/transport/stdio.rs @@ -21,7 +21,7 @@ use crate::{ pub async fn run() -> crate::error::Result<()> { let app_config = load_config()?; let agent_config = app_config.resolve(None)?; - let rag = RagContext::new()?; + let rag = RagContext::new(app_config.default_skills.clone())?; let state = Arc::new(AgentState::new(agent_config, app_config, rag).await?); acp::serve(Stdio::new(), state) diff --git a/src/transport/ws.rs b/src/transport/ws.rs index 1d67591..fb5c471 100644 --- a/src/transport/ws.rs +++ b/src/transport/ws.rs @@ -76,7 +76,7 @@ enum WsOutbound { pub async fn serve(host: String, port: u16) -> crate::error::Result<()> { let app_config = load_config()?; let agent_config = app_config.resolve(None)?; - let rag = RagContext::new()?; + let rag = RagContext::new(app_config.default_skills.clone())?; let state = Arc::new(AgentState::new(agent_config, app_config, rag).await?); let cors = CorsLayer::new() diff --git a/src/tui/app.rs b/src/tui/app.rs index 7b64307..e7659e5 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -449,17 +449,19 @@ impl App { ↑/↓ scroll · PgUp/PgDn page · Ctrl+C quit" .to_string(), )), - "sessions" => match RagContext::new().and_then(|r| r.history.list_conversations()) { - Ok(metas) if metas.is_empty() => { - self.push(ChatItem::SystemInfo("no sessions yet".to_string())); - } - Ok(metas) => { - self.sessions = metas; - self.picker_selected = 0; - self.push_screen(Screen::SessionPicker); + "sessions" => { + match RagContext::new(vec![]).and_then(|r| r.history.list_conversations()) { + Ok(metas) if metas.is_empty() => { + self.push(ChatItem::SystemInfo("no sessions yet".to_string())); + } + Ok(metas) => { + self.sessions = metas; + self.picker_selected = 0; + self.push_screen(Screen::SessionPicker); + } + Err(e) => self.push(ChatItem::Err(e.to_string())), } - Err(e) => self.push(ChatItem::Err(e.to_string())), - }, + } "config" => { let ac = &self.agent_config; let mut rows = vec![ @@ -624,7 +626,7 @@ impl App { let title = meta.title.as_deref().unwrap_or("(untitled)"); self.push(ChatItem::SystemInfo(format!("─── {title}"))); - match RagContext::new().and_then(|r| r.history.load_conversation(&meta.id)) { + match RagContext::new(vec![]).and_then(|r| r.history.load_conversation(&meta.id)) { Ok(conv) => { for msg in &conv.messages { match msg.role {